package openapi3

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
)

// IncludeOrigin specifies whether to include the origin of the OpenAPI elements.
// Deprecated: set Loader.IncludeOrigin instead. This global is read by NewLoader
// for backward compatibility but is not safe for concurrent use.
var IncludeOrigin = false

func failedToResolveRefFragmentPart(value, what string) error {
	return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value)
}

// Loader helps deserialize an OpenAPIv3 document
type Loader struct {
	// IsExternalRefsAllowed enables visiting other files. Enforced only when
	// ReadFromURIFunc is nil; a custom ReadFromURIFunc bypasses this flag and
	// owns the access policy itself — see ReadFromURIFunc.
	IsExternalRefsAllowed bool

	// IncludeOrigin enables recording the file/line/column of each OpenAPI element.
	// Prefer this over the package-level IncludeOrigin global, which is not safe for
	// concurrent use.
	IncludeOrigin bool

	// ReadFromURIFunc overrides how the loader reads a referenced file or URL.
	//
	// SECURITY: when a custom ReadFromURIFunc is set, IsExternalRefsAllowed is
	// NOT enforced — this function alone decides which locations may be read. A
	// func that reads whatever URI it is handed (e.g. by delegating to
	// DefaultReadFromURI) resolves external $refs even when IsExternalRefsAllowed
	// is false, which on untrusted documents enables local file reads
	// (`$ref: "/etc/passwd"`) and SSRF (`$ref: "http://169.254.169.254/..."`).
	// A custom func must apply its own scheme/host allowlist, or re-check
	// IsExternalRefsAllowed, before reading.
	ReadFromURIFunc ReadFromURIFunc

	// JoinFunc allows overriding how relative $ref paths are resolved against
	// a base path. When set, it is called instead of the default join logic
	// that uses path.Dir and path.Join. This is useful when loading specs from
	// non-filesystem sources (e.g. git objects, remote archives) where the base
	// path follows a different convention than filesystem paths.
	JoinFunc func(basePath *url.URL, relativePath *url.URL) *url.URL

	Context context.Context

	rootDir      string
	rootLocation string

	visitedPathItemRefs map[string]struct{}

	visitedDocuments map[string]*T

	visitedRefs map[string]struct{}
	visitedPath []string
	backtrack   map[string][]func(value any)
}

// NewLoader returns an empty Loader
func NewLoader() *Loader {
	return &Loader{
		Context:       context.Background(),
		IncludeOrigin: IncludeOrigin,
	}
}

func (loader *Loader) resetVisitedPathItemRefs() {
	loader.visitedPathItemRefs = make(map[string]struct{})
	loader.visitedRefs = make(map[string]struct{})
	loader.visitedPath = nil
	loader.backtrack = make(map[string][]func(value any))
}

// LoadFromURI loads a spec from a remote URL
func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
	loader.resetVisitedPathItemRefs()
	return loader.loadFromURIInternal(location)
}

// LoadFromFile loads a spec from a local file path
func (loader *Loader) LoadFromFile(location string) (*T, error) {
	loader.rootDir = path.Dir(location)
	return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)})
}

func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) {
	data, err := loader.readURL(location)
	if err != nil {
		return nil, err
	}
	return loader.loadFromDataWithPathInternal(data, location)
}

func (loader *Loader) allowsExternalRefs(ref string) (err error) {
	if !loader.IsExternalRefsAllowed {
		err = fmt.Errorf("encountered disallowed external reference: %q", ref)
	}
	return
}

func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element any) (*url.URL, error) {
	// IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc
	// is installed; otherwise the custom func owns the access policy (see the
	// SECURITY note on the ReadFromURIFunc field).
	if loader.ReadFromURIFunc == nil {
		if err := loader.allowsExternalRefs(ref); err != nil {
			return nil, err
		}
	}

	resolvedPath, err := loader.resolvePathWithRef(ref, rootPath)
	if err != nil {
		return nil, err
	}
	if frag := resolvedPath.Fragment; frag != "" {
		return nil, fmt.Errorf("unexpected ref fragment %q", frag)
	}

	data, err := loader.readURL(resolvedPath)
	if err != nil {
		return nil, err
	}
	if err := unmarshal(data, element, loader.IncludeOrigin, resolvedPath); err != nil {
		return nil, err
	}

	return resolvedPath, nil
}

func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
	if f := loader.ReadFromURIFunc; f != nil {
		return f(loader, location)
	}
	return DefaultReadFromURI(loader, location)
}

// LoadFromStdin loads a spec from stdin
func (loader *Loader) LoadFromStdin() (*T, error) {
	return loader.LoadFromIoReader(os.Stdin)
}

// LoadFromStdin loads a spec from io.Reader
func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) {
	if reader == nil {
		return nil, fmt.Errorf("invalid reader: %v", reader)
	}

	data, err := io.ReadAll(reader)
	if err != nil {
		return nil, err
	}
	return loader.LoadFromData(data)
}

// LoadFromData loads a spec from a byte array
func (loader *Loader) LoadFromData(data []byte) (*T, error) {
	loader.resetVisitedPathItemRefs()
	doc := &T{}
	if err := unmarshal(data, doc, loader.IncludeOrigin, nil); err != nil {
		return nil, err
	}
	if err := loader.ResolveRefsIn(doc, nil); err != nil {
		return nil, err
	}
	return doc, nil
}

// LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred
// elements and returns a *T with all resolved data or an error if unable to load data or resolve refs.
func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) {
	loader.resetVisitedPathItemRefs()
	return loader.loadFromDataWithPathInternal(data, location)
}

func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) {
	if loader.visitedDocuments == nil {
		loader.visitedDocuments = make(map[string]*T)
		loader.rootLocation = location.Path
	}
	uri := location.String()
	if doc, ok := loader.visitedDocuments[uri]; ok {
		return doc, nil
	}

	doc := &T{}
	loader.visitedDocuments[uri] = doc

	if err := unmarshal(data, doc, loader.IncludeOrigin, location); err != nil {
		return nil, err
	}

	doc.url = copyURI(location)

	if err := loader.ResolveRefsIn(doc, location); err != nil {
		return nil, err
	}

	return doc, nil
}

// ResolveRefsIn expands references if for instance spec was just unmarshaled
func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
	if loader.Context == nil {
		loader.Context = context.Background()
	}

	if loader.visitedPathItemRefs == nil {
		loader.resetVisitedPathItemRefs()
	}

	if components := doc.Components; components != nil {
		for _, name := range componentNames(components.Headers) {
			component := components.Headers[name]
			if err = loader.resolveHeaderRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.Parameters) {
			component := components.Parameters[name]
			if err = loader.resolveParameterRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.RequestBodies) {
			component := components.RequestBodies[name]
			if err = loader.resolveRequestBodyRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.Responses) {
			component := components.Responses[name]
			if err = loader.resolveResponseRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.Schemas) {
			component := components.Schemas[name]
			if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.SecuritySchemes) {
			component := components.SecuritySchemes[name]
			if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.Examples) {
			component := components.Examples[name]
			if err = loader.resolveExampleRef(doc, component, location); err != nil {
				return
			}
		}
		for _, name := range componentNames(components.Callbacks) {
			component := components.Callbacks[name]
			if err = loader.resolveCallbackRef(doc, component, location); err != nil {
				return
			}
		}
	}

	// Visit all operations
	pathItems := doc.Paths.Map()
	for _, name := range componentNames(pathItems) {
		pathItem := pathItems[name]
		if pathItem == nil {
			continue
		}
		if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
			return
		}
	}

	for _, name := range componentNames(doc.Webhooks) {
		if pathItem := doc.Webhooks[name]; pathItem != nil {
			if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
				return
			}
		}
	}

	return
}

func defaultJoin(basePath *url.URL, relativePath *url.URL) *url.URL {
	if basePath == nil {
		return relativePath
	}
	newPath := *basePath
	newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
	return &newPath
}

func (loader *Loader) resolvePath(basePath *url.URL, componentPath *url.URL) *url.URL {
	if is_file(componentPath) {
		// support absolute paths
		if filepath.IsAbs(componentPath.Path) {
			return componentPath
		}
		if loader.JoinFunc != nil {
			return loader.JoinFunc(basePath, componentPath)
		}
		return defaultJoin(basePath, componentPath)
	}
	return componentPath
}

func (loader *Loader) resolvePathWithRef(ref string, rootPath *url.URL) (*url.URL, error) {
	parsedURL, err := url.Parse(ref)
	if err != nil {
		return nil, fmt.Errorf("cannot parse reference: %q: %w", ref, err)
	}

	resolvedPath := loader.resolvePath(rootPath, parsedURL)
	resolvedPath.Fragment = parsedURL.Fragment
	return resolvedPath, nil
}

func (loader *Loader) resolveRefPath(ref string, path *url.URL) (*url.URL, error) {
	if ref != "" && ref[0] == '#' {
		path = copyURI(path)
		// Resolving internal refs of a doc loaded from memory
		// has no path, so just set the Fragment.
		if path == nil {
			path = new(url.URL)
		}

		path.Fragment = ref
		return path, nil
	}

	// IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc
	// is installed; otherwise the custom func owns the access policy (see the
	// SECURITY note on the ReadFromURIFunc field).
	if loader.ReadFromURIFunc == nil {
		if err := loader.allowsExternalRefs(ref); err != nil {
			return nil, err
		}
	}

	resolvedPath, err := loader.resolvePathWithRef(ref, path)
	if err != nil {
		return nil, err
	}

	return resolvedPath, nil
}

func isSingleRefElement(ref string) bool {
	return !strings.Contains(ref, "#")
}

func (loader *Loader) visitRef(ref string) {
	if loader.visitedRefs == nil {
		loader.visitedRefs = make(map[string]struct{})
		loader.backtrack = make(map[string][]func(value any))
	}
	loader.visitedPath = append(loader.visitedPath, ref)
	loader.visitedRefs[ref] = struct{}{}
}

func (loader *Loader) unvisitRef(ref string, value any) {
	if value != nil {
		for _, fn := range loader.backtrack[ref] {
			fn(value)
		}
	}
	delete(loader.visitedRefs, ref)
	delete(loader.backtrack, ref)
	loader.visitedPath = loader.visitedPath[:len(loader.visitedPath)-1]
}

func (loader *Loader) shouldVisitRef(ref string, fn func(value any)) bool {
	if _, ok := loader.visitedRefs[ref]; ok {
		loader.backtrack[ref] = append(loader.backtrack[ref], fn)
		return false
	}
	return true
}

func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved any) (
	componentDoc *T,
	componentPath *url.URL,
	err error,
) {
	if componentDoc, ref, componentPath, err = loader.resolveRefAndDocument(doc, ref, path); err != nil {
		return nil, nil, err
	}

	parsedURL, err := url.Parse(ref)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL)
	}
	fragment := parsedURL.Fragment
	if fragment == "" {
		fragment = "/"
	}
	if fragment[0] != '/' {
		return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref)
	}

	drill := func(cursor any) (any, error) {
		for pathPart := range strings.SplitSeq(fragment[1:], "/") {
			pathPart = unescapeRefString(pathPart)
			attempted := false

			switch c := cursor.(type) {
			// Special case of T
			// See issue856: a ref to doc => we assume that doc is a T => things live in T.Extensions
			case *T:
				if pathPart == "" {
					cursor = c.Extensions
					attempted = true
				}

			// Special case due to multijson
			case *SchemaRef:
				if pathPart == "additionalProperties" {
					if ap := c.Value.AdditionalProperties.Has; ap != nil {
						cursor = *ap
					} else {
						cursor = c.Value.AdditionalProperties.Schema
					}
					attempted = true
				}

			case *Responses:
				cursor = c.m // m map[string]*ResponseRef
			case *Callback:
				cursor = c.m // m map[string]*PathItem
			case *Paths:
				cursor = c.m // m map[string]*PathItem
			}

			if !attempted {
				if cursor, err = drillIntoField(cursor, pathPart); err != nil {
					e := failedToResolveRefFragmentPart(ref, pathPart)
					return nil, fmt.Errorf("%s: %w", e, err)
				}
			}

			if cursor == nil {
				return nil, failedToResolveRefFragmentPart(ref, pathPart)
			}
		}
		return cursor, nil
	}
	var cursor any
	if cursor, err = drill(componentDoc); err != nil {
		if path == nil {
			return nil, nil, err
		}
		var err2 error
		data, err2 := loader.readURL(path)
		if err2 != nil {
			return nil, nil, err
		}
		if err2 = unmarshal(data, &cursor, loader.IncludeOrigin, path); err2 != nil {
			return nil, nil, err
		}
		if cursor, err2 = drill(cursor); err2 != nil || cursor == nil {
			return nil, nil, err
		}
		err = nil
	}

	setPathRef := func(target any) {
		if i, ok := target.(interface {
			setRefPath(*url.URL)
		}); ok {
			pathRef := copyURI(componentPath)
			// Resolving internal refs of a doc loaded from memory
			// has no path, so just set the Fragment.
			if pathRef == nil {
				pathRef = new(url.URL)
			}
			pathRef.Fragment = fragment

			i.setRefPath(pathRef)
		}
	}

	switch {
	case reflect.TypeOf(cursor) == reflect.TypeOf(resolved):
		setPathRef(cursor)

		reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem())
		return componentDoc, componentPath, nil

	case reflect.TypeOf(cursor) == reflect.TypeFor[map[string]any]():
		codec := func(got, expect any) error {
			enc, err := json.Marshal(got)
			if err != nil {
				return err
			}
			if err = json.Unmarshal(enc, expect); err != nil {
				return err
			}

			setPathRef(expect)
			return nil
		}
		if err := codec(cursor, resolved); err != nil {
			return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
		}
		return componentDoc, componentPath, nil

	default:
		return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
	}
}

func readableType(x any) string {
	switch x.(type) {
	case *Callback:
		return "callback object"
	case *CallbackRef:
		return "ref to callback object"
	case *ExampleRef:
		return "ref to example object"
	case *HeaderRef:
		return "ref to header object"
	case *LinkRef:
		return "ref to link object"
	case *ParameterRef:
		return "ref to parameter object"
	case *PathItem:
		return "pathItem object"
	case *RequestBodyRef:
		return "ref to requestBody object"
	case *ResponseRef:
		return "ref to response object"
	case *SchemaRef:
		return "ref to schema object"
	case *SecuritySchemeRef:
		return "ref to securityScheme object"
	default:
		panic(fmt.Sprintf("unreachable %T", x))
	}
}

func drillIntoField(cursor any, fieldName string) (any, error) {
	switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {

	case reflect.Map:
		elementValue := val.MapIndex(reflect.ValueOf(fieldName))
		if !elementValue.IsValid() {
			return nil, fmt.Errorf("map key %q not found", fieldName)
		}
		return elementValue.Interface(), nil

	case reflect.Slice:
		i, err := strconv.ParseUint(fieldName, 10, 32)
		if err != nil {
			return nil, err
		}
		index := int(i)
		if 0 > index || index >= val.Len() {
			return nil, errors.New("slice index out of bounds")
		}
		return val.Index(index).Interface(), nil

	case reflect.Struct:
		hasFields := false
		for i := range val.NumField() {
			hasFields = true
			if yamlTag := val.Type().Field(i).Tag.Get("yaml"); yamlTag != "-" {
				if tagName, _, _ := strings.Cut(yamlTag, ","); tagName != "" {
					if fieldName == tagName {
						return val.Field(i).Interface(), nil
					}
				}
			}
		}

		// if cursor is a "ref wrapper" struct (e.g. RequestBodyRef),
		if _, ok := val.Type().FieldByName("Value"); ok {
			// try digging into its Value field
			return drillIntoField(val.FieldByName("Value").Interface(), fieldName)
		}
		if hasFields {
			if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" {
				extensions := val.Field(0).Interface().(map[string]any)
				if enc, ok := extensions[fieldName]; ok {
					return enc, nil
				}
			}
		}
		return nil, fmt.Errorf("struct field %q not found", fieldName)

	default:
		return nil, errors.New("not a map, slice nor struct")
	}
}

func (loader *Loader) resolveRefAndDocument(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
	if ref != "" && ref[0] == '#' {
		return doc, ref, path, nil
	}

	fragment, resolvedPath, err := loader.resolveRef(ref, path)
	if err != nil {
		return nil, "", nil, err
	}

	if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil {
		return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err)
	}

	return doc, fragment, resolvedPath, nil
}

func (loader *Loader) resolveRef(ref string, path *url.URL) (string, *url.URL, error) {
	resolvedPathRef, err := loader.resolveRefPath(ref, path)
	if err != nil {
		return "", nil, err
	}

	fragment := "#" + resolvedPathRef.Fragment
	resolvedPathRef.Fragment = ""
	return fragment, resolvedPathRef, nil
}

var (
	errMUSTCallback       = errors.New("invalid callback: value MUST be an object")
	errMUSTExample        = errors.New("invalid example: value MUST be an object")
	errMUSTHeader         = errors.New("invalid header: value MUST be an object")
	errMUSTLink           = errors.New("invalid link: value MUST be an object")
	errMUSTParameter      = errors.New("invalid parameter: value MUST be an object")
	errMUSTPathItem       = errors.New("invalid path item: value MUST be an object")
	errMUSTRequestBody    = errors.New("invalid requestBody: value MUST be an object")
	errMUSTResponse       = errors.New("invalid response: value MUST be an object")
	errMUSTSchema         = errors.New("invalid schema: value MUST be an object")
	errMUSTSecurityScheme = errors.New("invalid securityScheme: value MUST be an object")
)

func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTHeader
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Header)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var header Header
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil {
				return err
			}
			component.Value = &header
			component.setRefPath(documentPath)
		} else {
			var resolved HeaderRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTHeader {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	value := component.Value
	if value == nil {
		return nil
	}

	if schema := value.Schema; schema != nil {
		if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
			return err
		}
	}
	for _, k := range componentNames(value.Examples) {
		if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil {
			return err
		}
	}
	return nil
}

func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTParameter
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Parameter)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var param Parameter
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &param); err != nil {
				return err
			}
			component.Value = &param
			component.setRefPath(documentPath)
		} else {
			var resolved ParameterRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTParameter {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	value := component.Value
	if value == nil {
		return nil
	}

	if value.Content != nil && value.Schema != nil {
		return errors.New("cannot contain both schema and content in a parameter")
	}
	for _, name := range componentNames(value.Content) {
		contentType := value.Content[name]
		if schema := contentType.Schema; schema != nil {
			if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
				return err
			}
		}
		for _, k := range componentNames(contentType.Examples) {
			if err := loader.resolveExampleRef(doc, contentType.Examples[k], documentPath); err != nil {
				return err
			}
		}
	}
	if schema := value.Schema; schema != nil {
		if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
			return err
		}
	}
	for _, k := range componentNames(value.Examples) {
		if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil {
			return err
		}
	}
	return nil
}

func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTRequestBody
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*RequestBody)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var requestBody RequestBody
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil {
				return err
			}
			component.Value = &requestBody
			component.setRefPath(documentPath)
		} else {
			var resolved RequestBodyRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTRequestBody {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	value := component.Value
	if value == nil {
		return nil
	}

	for _, name := range componentNames(value.Content) {
		contentType := value.Content[name]
		if contentType == nil {
			continue
		}
		for _, name := range componentNames(contentType.Examples) {
			example := contentType.Examples[name]
			if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
				return err
			}
			contentType.Examples[name] = example
		}
		if schema := contentType.Schema; schema != nil {
			if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
				return err
			}
		}
	}
	return nil
}

func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTResponse
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Response)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var resp Response
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil {
				return err
			}
			component.Value = &resp
			component.setRefPath(documentPath)
		} else {
			var resolved ResponseRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTResponse {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	value := component.Value
	if value == nil {
		return nil
	}

	for _, name := range componentNames(value.Headers) {
		header := value.Headers[name]
		if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil {
			return err
		}
	}
	for _, name := range componentNames(value.Content) {
		contentType := value.Content[name]
		if contentType == nil {
			continue
		}
		for _, name := range componentNames(contentType.Examples) {
			example := contentType.Examples[name]
			if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
				return err
			}
			contentType.Examples[name] = example
		}
		if schema := contentType.Schema; schema != nil {
			if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
				return err
			}
			contentType.Schema = schema
		}
	}
	for _, name := range componentNames(value.Links) {
		link := value.Links[name]
		if err := loader.resolveLinkRef(doc, link, documentPath); err != nil {
			return err
		}
	}
	return nil
}

func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL, visited []string) (err error) {
	if component.isEmpty() {
		return errMUSTSchema
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Schema)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var schema Schema
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {
				return err
			}
			component.Value = &schema
			component.setRefPath(documentPath)
		} else {
			var resolved SchemaRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveSchemaRef(doc, &resolved, componentPath, visited); err != nil {
				if err == errMUSTSchema {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)

		// OAS 3.1 / JSON Schema 2020-12: apply sibling keywords from the original schema
		// object on top of the resolved $ref value. In 3.1, siblings are not ignored —
		// they augment the referenced schema (e.g. deprecated:true alongside $ref).
		// Only apply for OAS 3.1+ — in 3.0 $ref replaces its entire object and siblings
		// are (validly) ignored.
		if doc.IsOpenAPI31OrLater() && component.sibling != nil && component.Value != nil {
			// Work on a copy so we don't mutate a schema shared by other references.
			schemaCopy := *component.Value
			applySiblingSchemaFields(&schemaCopy, component.sibling, component.extra)
			component.Value = &schemaCopy
		}
	}
	value := component.Value
	if value == nil {
		return nil
	}

	// ResolveRefs referred schemas
	if v := value.Items; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, name := range componentNames(value.Properties) {
		v := value.Properties[name]
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.AdditionalProperties.Schema; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.Not; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, v := range value.AllOf {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, v := range value.AnyOf {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, v := range value.OneOf {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	// Discriminator mapping refs are a special case since they are not full
	// ref objects but are plain strings that reference schema objects.
	// Only resolve refs that look like external references (contain a path).
	// Plain schema names like "Dog" or internal refs like "#/components/schemas/Dog"
	// don't need to be resolved by the loader.
	if value.Discriminator != nil {
		for _, k := range componentNames(value.Discriminator.Mapping) {
			v := value.Discriminator.Mapping[k]
			// Only resolve if it looks like an external ref (contains path separator)
			if strings.Contains(v.Ref, "/") && !strings.HasPrefix(v.Ref, "#") {
				if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil {
					return err
				}
				value.Discriminator.Mapping[k] = v
			}
		}
	}

	// OpenAPI 3.1 / JSON Schema 2020-12 fields
	for _, v := range value.PrefixItems {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.Contains; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, name := range componentNames(value.PatternProperties) {
		v := value.PatternProperties[name]
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, name := range componentNames(value.DependentSchemas) {
		v := value.DependentSchemas[name]
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	for _, name := range componentNames(value.Defs) {
		v := value.Defs[name]
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.PropertyNames; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.UnevaluatedItems.Schema; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.UnevaluatedProperties.Schema; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.If; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.Then; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.Else; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}
	if v := value.ContentSchema; v != nil {
		if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
			return err
		}
	}

	return nil
}

func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTSecurityScheme
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*SecurityScheme)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var scheme SecurityScheme
			if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
				return err
			}
			component.Value = &scheme
			component.setRefPath(documentPath)
		} else {
			var resolved SecuritySchemeRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTSecurityScheme {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	return nil
}

func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) {
	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Example)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var example Example
			if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
				return err
			}
			component.Value = &example
			component.setRefPath(documentPath)
		} else {
			var resolved ExampleRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTExample {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	return nil
}

func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTCallback
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Callback)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var resolved Callback
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil {
				return err
			}
			component.Value = &resolved
			component.setRefPath(documentPath)
		} else {
			var resolved CallbackRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err = loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTCallback {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	value := component.Value
	if value == nil {
		return nil
	}

	pathItems := value.Map()
	for _, name := range componentNames(pathItems) {
		pathItem := pathItems[name]
		if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil {
			return err
		}
	}
	return nil
}

func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) {
	if component.isEmpty() {
		return errMUSTLink
	}

	if ref := component.Ref; ref != "" {
		if component.Value != nil {
			return nil
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			component.Value = value.(*Link)
			refPath, _ := loader.resolveRefPath(ref, documentPath)
			component.setRefPath(refPath)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var link Link
			if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
				return err
			}
			component.Value = &link
			component.setRefPath(documentPath)
		} else {
			var resolved LinkRef
			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
			if err != nil {
				return err
			}
			if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil {
				if err == errMUSTLink {
					return nil
				}
				return err
			}
			component.Value = resolved.Value
			component.setRefPath(resolved.RefPath())
		}
		defer loader.unvisitRef(ref, component.Value)
	}
	return nil
}

func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) {
	if pathItem == nil {
		err = errMUSTPathItem
		return
	}

	if ref := pathItem.Ref; ref != "" {
		if !pathItem.isEmpty() {
			return
		}
		if !loader.shouldVisitRef(ref, func(value any) {
			*pathItem = *value.(*PathItem)
		}) {
			return nil
		}
		loader.visitRef(ref)
		if isSingleRefElement(ref) {
			var p PathItem
			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
				return
			}
			*pathItem = p
		} else {
			var resolved PathItem
			if doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved); err != nil {
				if err == errMUSTPathItem {
					return nil
				}
				return
			}
			*pathItem = resolved
		}
		pathItem.Ref = ref
		defer loader.unvisitRef(ref, pathItem)
	}

	for _, parameter := range pathItem.Parameters {
		if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
			return
		}
	}
	operations := pathItem.Operations()
	for _, name := range componentNames(operations) {
		operation := operations[name]
		for _, parameter := range operation.Parameters {
			if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
				return
			}
		}
		if requestBody := operation.RequestBody; requestBody != nil {
			if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil {
				return
			}
		}
		responses := operation.Responses.Map()
		for _, name := range componentNames(responses) {
			response := responses[name]
			if err = loader.resolveResponseRef(doc, response, documentPath); err != nil {
				return
			}
		}
		for _, name := range componentNames(operation.Callbacks) {
			callback := operation.Callbacks[name]
			if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil {
				return
			}
		}
	}
	return
}

func unescapeRefString(ref string) string {
	return strings.ReplaceAll(strings.ReplaceAll(ref, "~1", "/"), "~0", "~")
}

// applySiblingSchemaFields overlays the fields listed in presentFields from sibling onto dst.
// It is used to honour keyword siblings of $ref in OpenAPI 3.1 / JSON Schema 2020-12, where
// sibling keywords are applied in addition to (not instead of) the referenced schema.
// Only fields that were explicitly present in the original YAML/JSON are applied; the presentFields
// slice (derived from SchemaRef.extra) carries this information.
func applySiblingSchemaFields(dst, sibling *Schema, presentFields []string) {
	for _, field := range presentFields {
		switch field {
		case "oneOf":
			dst.OneOf = sibling.OneOf
		case "anyOf":
			dst.AnyOf = sibling.AnyOf
		case "allOf":
			dst.AllOf = sibling.AllOf
		case "not":
			dst.Not = sibling.Not
		case "type":
			dst.Type = sibling.Type
		case "title":
			dst.Title = sibling.Title
		case "format":
			dst.Format = sibling.Format
		case "description":
			dst.Description = sibling.Description
		case "enum":
			dst.Enum = sibling.Enum
		case "default":
			dst.Default = sibling.Default
		case "example":
			dst.Example = sibling.Example
		case "externalDocs":
			dst.ExternalDocs = sibling.ExternalDocs
		case "uniqueItems":
			dst.UniqueItems = sibling.UniqueItems
		case "exclusiveMinimum":
			dst.ExclusiveMin = sibling.ExclusiveMin
		case "exclusiveMaximum":
			dst.ExclusiveMax = sibling.ExclusiveMax
		case "nullable":
			dst.Nullable = sibling.Nullable
		case "readOnly":
			dst.ReadOnly = sibling.ReadOnly
		case "writeOnly":
			dst.WriteOnly = sibling.WriteOnly
		case "allowEmptyValue":
			dst.AllowEmptyValue = sibling.AllowEmptyValue
		case "deprecated":
			dst.Deprecated = sibling.Deprecated
		case "xml":
			dst.XML = sibling.XML
		case "minimum":
			dst.Min = sibling.Min
		case "maximum":
			dst.Max = sibling.Max
		case "multipleOf":
			dst.MultipleOf = sibling.MultipleOf
		case "minLength":
			dst.MinLength = sibling.MinLength
		case "maxLength":
			dst.MaxLength = sibling.MaxLength
		case "pattern":
			dst.Pattern = sibling.Pattern
		case "minItems":
			dst.MinItems = sibling.MinItems
		case "maxItems":
			dst.MaxItems = sibling.MaxItems
		case "items":
			dst.Items = sibling.Items
		case "required":
			dst.Required = sibling.Required
		case "properties":
			dst.Properties = sibling.Properties
		case "minProperties":
			dst.MinProps = sibling.MinProps
		case "maxProperties":
			dst.MaxProps = sibling.MaxProps
		case "additionalProperties":
			dst.AdditionalProperties = sibling.AdditionalProperties
		case "discriminator":
			dst.Discriminator = sibling.Discriminator
		case "const":
			dst.Const = sibling.Const
		case "examples":
			dst.Examples = sibling.Examples
		case "prefixItems":
			dst.PrefixItems = sibling.PrefixItems
		case "contains":
			dst.Contains = sibling.Contains
		case "minContains":
			dst.MinContains = sibling.MinContains
		case "maxContains":
			dst.MaxContains = sibling.MaxContains
		case "patternProperties":
			dst.PatternProperties = sibling.PatternProperties
		case "dependentSchemas":
			dst.DependentSchemas = sibling.DependentSchemas
		case "propertyNames":
			dst.PropertyNames = sibling.PropertyNames
		case "unevaluatedItems":
			dst.UnevaluatedItems = sibling.UnevaluatedItems
		case "unevaluatedProperties":
			dst.UnevaluatedProperties = sibling.UnevaluatedProperties
		case "if":
			dst.If = sibling.If
		case "then":
			dst.Then = sibling.Then
		case "else":
			dst.Else = sibling.Else
		case "dependentRequired":
			dst.DependentRequired = sibling.DependentRequired
		case "$defs":
			dst.Defs = sibling.Defs
		case "$schema":
			dst.SchemaDialect = sibling.SchemaDialect
		case "$comment":
			dst.Comment = sibling.Comment
		case "$id":
			dst.SchemaID = sibling.SchemaID
		case "$anchor":
			dst.Anchor = sibling.Anchor
		case "$dynamicRef":
			dst.DynamicRef = sibling.DynamicRef
		case "$dynamicAnchor":
			dst.DynamicAnchor = sibling.DynamicAnchor
		case "contentMediaType":
			dst.ContentMediaType = sibling.ContentMediaType
		case "contentEncoding":
			dst.ContentEncoding = sibling.ContentEncoding
		case "contentSchema":
			dst.ContentSchema = sibling.ContentSchema
		}
	}
}
