// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package handlers

import (
	"encoding/json"
	"go/token"
	"regexp"
	"strings"

	"github.com/go-openapi/codescan/internal/builders/resolvers"
	"github.com/go-openapi/codescan/internal/builders/validations"
	"github.com/go-openapi/codescan/internal/ifaces"
	"github.com/go-openapi/codescan/internal/parsers/grammar"
	yamlparser "github.com/go-openapi/codescan/internal/parsers/yaml"
	oaispec "github.com/go-openapi/spec"
)

var _ ifaces.ValidationBuilder = &SchemaValidations{}

// SchemaOptions configures the full-Schema dispatch.
//
// SimpleSchemaMode enables the SimpleSchema gating used by the parameter/header drivers that pass
// through the schema builder: full-Schema-only keywords (readOnly, discriminator, ...) emit
// CodeUnsupportedInSimpleSchema and skip the write; `required:` is silently skipped.
//
// See [§simple-schema-keywords](./README.md#simple-schema-keywords).
type SchemaOptions struct {
	SimpleSchemaMode bool
}

// SchemaValidations adapts *oaispec.Schema to ifaces.ValidationBuilder so the full-Schema handler
// family can write level-0 and items-level validations through a uniform target.
//
// Exported because the schema package's refOverrideCollector also uses it directly when building
// $ref-override allOf compounds.
type SchemaValidations struct {
	current *oaispec.Schema
}

// NewSchemaValidations builds an adapter around ps.
func NewSchemaValidations(ps *oaispec.Schema) SchemaValidations {
	return SchemaValidations{current: ps}
}

func (sv SchemaValidations) SetMaximum(val float64, exclusive bool) {
	sv.current.Maximum = &val
	sv.current.ExclusiveMaximum = exclusive
}

func (sv SchemaValidations) SetMinimum(val float64, exclusive bool) {
	sv.current.Minimum = &val
	sv.current.ExclusiveMinimum = exclusive
}
func (sv SchemaValidations) SetMultipleOf(val float64)  { sv.current.MultipleOf = &val }
func (sv SchemaValidations) SetMinItems(val int64)      { sv.current.MinItems = &val }
func (sv SchemaValidations) SetMaxItems(val int64)      { sv.current.MaxItems = &val }
func (sv SchemaValidations) SetMinProperties(val int64) { sv.current.MinProperties = &val }
func (sv SchemaValidations) SetMaxProperties(val int64) { sv.current.MaxProperties = &val }
func (sv SchemaValidations) SetMinLength(val int64)     { sv.current.MinLength = &val }
func (sv SchemaValidations) SetMaxLength(val int64)     { sv.current.MaxLength = &val }
func (sv SchemaValidations) SetPattern(val string)      { sv.current.Pattern = val }

// SetPatternProperties adds one regex → empty-schema entry to the schema's patternProperties map.
//
// The empty value schema (`{}`) means "properties whose name matches pattern are allowed (any
// value)" — the annotation surface carries only the regex, not a per-pattern value schema.
// Repeated patternProperties lines accumulate distinct entries.
func (sv SchemaValidations) SetPatternProperties(pattern string) {
	if sv.current.PatternProperties == nil {
		sv.current.PatternProperties = make(oaispec.SchemaProperties)
	}
	sv.current.PatternProperties[pattern] = oaispec.Schema{}
}
func (sv SchemaValidations) SetUnique(val bool) { sv.current.UniqueItems = val }
func (sv SchemaValidations) SetDefault(val any) { sv.current.Default = val }
func (sv SchemaValidations) SetExample(val any) { sv.current.Example = val }

// SetEnum writes the parsed enum onto the schema and strips any inherited x-go-enum-desc that the
// field-level override has made stale — see [§stale-enum-desc](./README.md#stale-enum-desc).
func (sv SchemaValidations) SetEnum(val string) {
	var typ string
	if len(sv.current.Type) > 0 {
		typ = sv.current.Type[0]
	}
	sv.current.Enum = validations.ParseEnumValues(val, typ, sv.current.Format)
	clearStaleEnumDesc(sv.current)
}

// DispatchSchemaLevel0 routes every level-0 Property in block into ps.
//
// The enclosing schema receives required/discriminator writes keyed by name (name == "" silently
// skips those cross-target writes, for the top-level model case where there is no enclosing).
//
// opts.SimpleSchemaMode gates full-Schema-only keywords with CodeUnsupportedInSimpleSchema (for the
// parameters/headers paths that drive the schema builder under SimpleSchema constraints).
//
// diag may be nil; when nil, all diagnostics are dropped.
func DispatchSchemaLevel0(block grammar.Block, enclosing, ps *oaispec.Schema, name string,
	diag func(grammar.Diagnostic), opts SchemaOptions,
) {
	valid := NewSchemaValidations(ps)

	block.Walk(grammar.Walker{
		FilterDepth: 0,
		Number:      schemaNumberHandler(ps, valid, diag),
		Integer:     schemaIntegerHandler(ps, valid, diag),
		Bool:        schemaBoolHandler(enclosing, ps, name, valid, diag, opts),
		String:      schemaStringHandler(ps, valid, diag),
		Raw:         schemaRawHandler(ps, valid, diag, opts),
		Extension:   Extension(ps),
		Diagnostic:  diag,
	})
}

// DispatchSchemaItemsLevel dispatches items-depth Property entries onto target.
//
// Items elements don't carry vendor extensions and don't participate in cross-target writes
// (required/discriminator), so the dispatcher is narrower than the level-0 entry — only number/
// integer/bool(unique)/string/raw handlers fire.
//
// opts.SimpleSchemaMode is threaded to the raw handler so the full-Schema-only externalDocs keyword
// is gated consistently; the other items-level handlers are unaffected by it.
//
// diag may be nil; when nil, all diagnostics are dropped.
func DispatchSchemaItemsLevel(block grammar.Block, target *oaispec.Schema, depth int,
	diag func(grammar.Diagnostic), opts SchemaOptions,
) {
	valid := NewSchemaValidations(target)

	block.Walk(grammar.Walker{
		FilterDepth: depth,
		Number:      schemaNumberHandler(target, valid, diag),
		Integer:     schemaIntegerHandler(target, valid, diag),
		Bool: func(p grammar.Property, val bool) {
			if !p.IsTyped() {
				return
			}
			if !checkShape(p, target, diag) {
				return
			}
			if p.Keyword.Name == grammar.KwUnique {
				valid.SetUnique(val)
			}
		},
		String:     schemaStringHandler(target, valid, diag),
		Raw:        schemaRawHandler(target, valid, diag, opts),
		Diagnostic: diag,
	})
}

// ApplyPattern stores a regex pattern on valid and runs a best-effort RE2 hygiene check, emitting
// CodeInvalidAnnotation when the regex does not compile in Go's RE2 engine.
//
// The pattern is kept on the schema regardless — downstream tools may use JSON Schema's wider
// regex dialect.
//
// Exported because the schema package's refOverrideCollector applies the same pattern semantics
// when building $ref-override allOf compounds.
//
// diag may be nil; when nil, the RE2 mismatch warning is dropped.
func ApplyPattern(p grammar.Property, valid SchemaValidations, val string, diag func(grammar.Diagnostic)) {
	valid.SetPattern(val)
	warnInvalidRE2(p, grammar.KwPattern, val, diag)
}

// ApplyPatternProperties adds a regex → empty-schema entry to the schema's patternProperties map
// and runs the same best-effort RE2 hygiene check as ApplyPattern: a regex that does not compile in
// Go's RE2 engine emits CodeInvalidAnnotation but is preserved on the schema (downstream tools may
// use JSON Schema's wider regex dialect).
//
// Exported for symmetry with ApplyPattern; the schema package's refOverrideCollector applies the
// same semantics on $ref overrides.
//
// diag may be nil; when nil, the RE2 mismatch warning is dropped.
func ApplyPatternProperties(p grammar.Property, valid SchemaValidations, val string, diag func(grammar.Diagnostic)) {
	valid.SetPatternProperties(val)
	warnInvalidRE2(p, grammar.KwPatternProperties, val, diag)
}

// warnInvalidRE2 emits a CodeInvalidAnnotation warning when val is a non-empty string that does not
// compile under Go's RE2 engine.
//
// The keyword name is used to prefix the message so pattern and patternProperties produce parallel
// diagnostics.
// No-op on empty val or nil diag — the value is always preserved on the schema by the caller regardless.
func warnInvalidRE2(p grammar.Property, keyword, val string, diag func(grammar.Diagnostic)) {
	if val == "" || diag == nil {
		return
	}
	if _, err := regexp.Compile(val); err != nil {
		diag(grammar.Diagnostic{
			Pos:      p.Pos,
			Severity: grammar.SeverityWarning,
			Code:     grammar.CodeInvalidAnnotation,
			Message: strings.TrimSpace(
				keyword + ": " + val + " is not a valid Go RE2 regex (" + err.Error() + "); " +
					"the value is preserved on the schema but downstream RE2 validators will fail",
			),
		})
	}
}

// SetRequired adds or removes name from the enclosing schema's Required slice.
//
// Exported because the schema package's refOverrideCollector reuses this when handling `required:`
// on a $ref-override field.
func SetRequired(enclosing *oaispec.Schema, name string, required bool) {
	if enclosing == nil {
		return
	}
	midx := -1
	for i, nm := range enclosing.Required {
		if nm == name {
			midx = i
			break
		}
	}
	if required {
		if midx < 0 {
			enclosing.Required = append(enclosing.Required, name)
		}
		return
	}
	if midx >= 0 {
		enclosing.Required = append(enclosing.Required[:midx], enclosing.Required[midx+1:]...)
	}
}

// SetDiscriminator writes name to enclosing.Discriminator when required=true, or clears it when
// required=false and the current value matches.
func SetDiscriminator(enclosing *oaispec.Schema, name string, required bool) {
	if enclosing == nil {
		return
	}
	if required {
		enclosing.Discriminator = name
		return
	}
	if enclosing.Discriminator == name {
		enclosing.Discriminator = ""
	}
}

// SchemaTypeOf returns the resolved Swagger type of ps, or "" when the schema has no Type set
// (typically a $ref-only schema, where type-aware coercion is not possible at this level).
func SchemaTypeOf(ps *oaispec.Schema) string {
	if ps == nil || len(ps.Type) == 0 {
		return ""
	}
	return ps.Type[0]
}

// --- internal per-shape handler factories ---

// schemaNumberHandler returns a Walker.Number callback bound to valid.
//
// Recognises maximum / minimum / multipleOf.
// Skips on typing failure (parser already emitted a CodeInvalidNumber) or shape mismatch
// (checkShape emits CodeShapeMismatch and the property is dropped).
func schemaNumberHandler(ps *oaispec.Schema, valid SchemaValidations,
	diag func(grammar.Diagnostic),
) func(grammar.Property, float64, bool) {
	return func(p grammar.Property, val float64, exclusive bool) {
		if !p.IsTyped() {
			return
		}
		if !checkShape(p, ps, diag) {
			return
		}
		switch p.Keyword.Name {
		case grammar.KwMaximum:
			valid.SetMaximum(val, exclusive)
		case grammar.KwMinimum:
			valid.SetMinimum(val, exclusive)
		case grammar.KwMultipleOf:
			valid.SetMultipleOf(val)
		}
	}
}

// schemaIntegerHandler returns a Walker.Integer callback bound to valid.
//
// Recognises minLength / maxLength / minItems / maxItems / minProperties / maxProperties.
// Skips on typing failure or shape mismatch.
func schemaIntegerHandler(ps *oaispec.Schema, valid SchemaValidations,
	diag func(grammar.Diagnostic),
) func(grammar.Property, int64) {
	return func(p grammar.Property, val int64) {
		if !p.IsTyped() {
			return
		}
		if !checkShape(p, ps, diag) {
			return
		}
		switch p.Keyword.Name {
		case grammar.KwMinLength:
			valid.SetMinLength(val)
		case grammar.KwMaxLength:
			valid.SetMaxLength(val)
		case grammar.KwMinItems:
			valid.SetMinItems(val)
		case grammar.KwMaxItems:
			valid.SetMaxItems(val)
		case grammar.KwMinProperties:
			valid.SetMinProperties(val)
		case grammar.KwMaxProperties:
			valid.SetMaxProperties(val)
		}
	}
}

// schemaBoolHandler returns a Walker.Bool callback.
//
// Required and discriminator route to the enclosing schema keyed by name; unique/readOnly route to
// the property schema.
//
// Under SimpleSchemaMode, full-Schema-only keywords (readOnly, discriminator) emit
// CodeUnsupportedInSimpleSchema and skip; `required:` is silently skipped
// — see [§simple-schema-keywords](./README.md#simple-schema-keywords).
func schemaBoolHandler(enclosing, ps *oaispec.Schema, name string, valid SchemaValidations,
	diag func(grammar.Diagnostic), opts SchemaOptions,
) func(grammar.Property, bool) {
	return func(p grammar.Property, val bool) {
		if !p.IsTyped() {
			return
		}
		if !checkShape(p, ps, diag) {
			return
		}
		if opts.SimpleSchemaMode && !IsSimpleSchemaKeyword(p.Keyword.Name) {
			if diag != nil {
				diag(grammar.Warnf(
					p.Pos,
					grammar.CodeUnsupportedInSimpleSchema,
					"%q is a full-Schema-only keyword and is not allowed under SimpleSchema mode; ignored",
					p.Keyword.Name,
				))
			}
			return
		}
		if opts.SimpleSchemaMode && p.Keyword.Name == grammar.KwRequired {
			return
		}
		switch p.Keyword.Name {
		case grammar.KwUnique:
			valid.SetUnique(val)
		case grammar.KwReadOnly:
			ps.ReadOnly = val
		case grammar.KwRequired:
			if name != "" {
				SetRequired(enclosing, name, val)
			}
		case grammar.KwDiscriminator:
			if name != "" {
				SetDiscriminator(enclosing, name, val)
			}
		}
	}
}

// schemaStringHandler returns a Walker.String callback.
//
// Recognises pattern (raw regex), patternProperties (object regex key) and the enum keyword's
// pre-typed enum-option form (rare; comma-list / JSON-array forms travel through Raw).
//
// Shape-checks pattern (string-only) and patternProperties (object-only); default/example/enum are
// type-independent (ParseDefault handles the type-specific coercion).
func schemaStringHandler(ps *oaispec.Schema, valid SchemaValidations,
	diag func(grammar.Diagnostic),
) func(grammar.Property, string) {
	return func(p grammar.Property, val string) {
		if !checkShape(p, ps, diag) {
			return
		}
		switch p.Keyword.Name {
		case grammar.KwPattern:
			ApplyPattern(p, valid, val, diag)
		case grammar.KwPatternProperties:
			ApplyPatternProperties(p, valid, val, diag)
		case grammar.KwDefault:
			if v, err := validations.ParseDefault(val, SchemaTypeOf(ps), ps.Format); err == nil {
				valid.SetDefault(v)
			} else {
				warnUncoercible(p.Pos, diag, grammar.KwDefault, val, SchemaTypeOf(ps))
			}
		case grammar.KwExample:
			if v, err := validations.ParseDefault(val, SchemaTypeOf(ps), ps.Format); err == nil {
				valid.SetExample(v)
			} else {
				warnUncoercible(p.Pos, diag, grammar.KwExample, val, SchemaTypeOf(ps))
			}
		case grammar.KwEnum:
			valid.SetEnum(val)
			pruneUncoercibleEnum(ps, p.Pos, diag)
		}
	}
}

// schemaRawHandler routes ShapeRawBlock / ShapeRawValue / ShapeNone / ShapeCommaList properties —
// default:, example:, enum: when expressed as raw bodies, plus externalDocs: (a YAML map body).
//
// Extensions travel through Walker.Extension with YAML-typed values, so the KwExtensions arm is
// absent here.
//
// externalDocs is full-Schema-only: under opts.SimpleSchemaMode it emits
// CodeUnsupportedInSimpleSchema and is dropped (mirroring the Bool handler's readOnly/discriminator
// gating).
// A malformed body emits CodeInvalidAnnotation. diag may be nil.
func schemaRawHandler(ps *oaispec.Schema, valid SchemaValidations,
	diag func(grammar.Diagnostic), opts SchemaOptions,
) func(grammar.Property) {
	return func(p grammar.Property) {
		switch p.Keyword.Name {
		case grammar.KwDefault:
			if v, err := validations.ParseDefault(p.Value, SchemaTypeOf(ps), ps.Format); err == nil {
				valid.SetDefault(v)
			} else {
				warnUncoercible(p.Pos, diag, grammar.KwDefault, p.Value, SchemaTypeOf(ps))
			}
		case grammar.KwExample:
			if v, err := validations.ParseDefault(p.Value, SchemaTypeOf(ps), ps.Format); err == nil {
				valid.SetExample(v)
			} else {
				warnUncoercible(p.Pos, diag, grammar.KwExample, p.Value, SchemaTypeOf(ps))
			}
		case grammar.KwEnum:
			valid.SetEnum(p.Value)
			pruneUncoercibleEnum(ps, p.Pos, diag)
		case grammar.KwExternalDocs:
			if opts.SimpleSchemaMode {
				if diag != nil {
					diag(grammar.Warnf(
						p.Pos,
						grammar.CodeUnsupportedInSimpleSchema,
						"%q is a full-Schema-only keyword and is not allowed under SimpleSchema mode; ignored",
						p.Keyword.Name,
					))
				}
				return
			}
			ed, err := ParseExternalDocs(p.Body)
			if err != nil {
				if diag != nil {
					diag(grammar.Warnf(p.Pos, grammar.CodeInvalidAnnotation, "externalDocs: %v", err))
				}
				return
			}
			if ed != nil {
				ps.ExternalDocs = ed
			}
		}
	}
}

// ParseExternalDocs unmarshals an `externalDocs:` block body (a YAML map with description/url keys)
// into a *spec.ExternalDocumentation.
//
// Returns (nil, nil) for an empty/blank block so callers don't emit a useless `externalDocs: {}`
// (the OAS object requires url).
// Shared by the schema raw handler and the routes builder.
func ParseExternalDocs(body string) (*oaispec.ExternalDocumentation, error) {
	var out *oaispec.ExternalDocumentation
	err := yamlparser.UnmarshalBody(body, func(data []byte) error {
		var d oaispec.ExternalDocumentation
		if err := json.Unmarshal(data, &d); err != nil {
			return err
		}
		if d != (oaispec.ExternalDocumentation{}) {
			out = &d
		}
		return nil
	})
	return out, err
}

// checkShape gates a Walker callback on validations.IsLegalForType(p.Keyword, schema-type).
//
// On mismatch, emits a CodeShapeMismatch diagnostic and returns false so the caller drops the property.
func checkShape(p grammar.Property, ps *oaispec.Schema, diag func(grammar.Diagnostic)) bool {
	var schemaType string
	if ps != nil && len(ps.Type) > 0 {
		schemaType = ps.Type[0]
	}
	ok, hint := validations.IsLegalForType(p.Keyword, schemaType)
	if ok {
		return true
	}
	if diag != nil {
		diag(grammar.Diagnostic{
			Pos:      p.Pos,
			Severity: grammar.SeverityWarning,
			Code:     grammar.CodeShapeMismatch,
			Message:  hint,
		})
	}
	return false
}

// RecoerceDeclValues runs a value coercion.
//
// Re-coercion is safe precisely because the fallback preserved the author's raw text verbatim: a
// value still stored as a string is one that was never typed. A string-typed schema is skipped —
// there the fallback and the correct answer coincide, so there is nothing to redo.
//
// Runs beside RecheckSchemaShape, at the same "the type is known now" seam.
func RecoerceDeclValues(sch *oaispec.Schema, pos token.Position, diag func(grammar.Diagnostic)) {
	if sch == nil || len(sch.Type) == 0 {
		return
	}
	typ := sch.Type[0]
	if typ == "string" {
		// Coercion TOWARDS string never fails — every literal has a string form — so the fallback and
		// the correct answer coincide here. Nothing to redo, and nothing that can be diagnosed.
		return
	}

	if raw, ok := sch.Default.(string); ok {
		v, err := validations.ParseDefault(raw, typ, sch.Format)
		if err != nil {
			warnUncoercible(pos, diag, grammar.KwDefault, raw, typ)
			sch.Default = nil
		} else {
			sch.Default = v
		}
	}
	if raw, ok := sch.Example.(string); ok {
		v, err := validations.ParseDefault(raw, typ, sch.Format)
		if err != nil {
			warnUncoercible(pos, diag, grammar.KwExample, raw, typ)
			sch.Example = nil
		} else {
			sch.Example = v
		}
	}

	// Enum members were coerced individually against the empty type, so each is independently a
	// string; re-coerce per member.
	for i, member := range sch.Enum {
		raw, ok := member.(string)
		if !ok {
			continue
		}
		if v, err := validations.CoerceValue(raw, &oaispec.SimpleSchema{Type: typ}); err == nil {
			sch.Enum[i] = v
		}
	}
	pruneUncoercibleEnum(sch, pos, diag)
}

// warnUncoercible reports a value that could not be read as the schema's type and was dropped.
//
// Dropping beats keeping: a value of the wrong type emits a document no validator accepts, whereas
// dropping it emits an incomplete one — the lesser harm, now that the author is told.
func warnUncoercible(pos token.Position, diag func(grammar.Diagnostic), keyword, raw, typ string) {
	if diag == nil {
		return
	}
	diag(grammar.Warnf(pos, grammar.CodeShapeMismatch,
		"%s: %q cannot be read as %s; value dropped", keyword, raw, typ))
}

// pruneUncoercibleEnum drops enum members that could not be read as the schema's type, warning for
// each one by name.
//
// CoerceEnum falls back to the raw string per member, so a member still stored as a string on a
// non-string schema is one that failed — `enum: 1, two, 3` on an integer schema yields
// [1, "two", 3], which no validator accepts.
//
// Dropping narrows a closed set, which is a real change to the author's contract, so the warning
// names the member rather than merely counting. It fails closed, matching what the `swagger:enum`
// annotation already does when a const value does not fit.
func pruneUncoercibleEnum(sch *oaispec.Schema, pos token.Position, diag func(grammar.Diagnostic)) {
	if sch == nil || len(sch.Enum) == 0 || len(sch.Type) == 0 {
		return
	}
	typ := sch.Type[0]
	if typ == "string" {
		return
	}

	kept := make([]any, 0, len(sch.Enum))
	for _, member := range sch.Enum {
		if raw, isString := member.(string); isString {
			warnUncoercible(pos, diag, grammar.KwEnum, raw, typ)

			continue
		}
		kept = append(kept, member)
	}
	if len(kept) == len(sch.Enum) {
		return
	}
	if len(kept) == 0 {
		sch.Enum = nil

		return
	}
	sch.Enum = kept
}

// RecheckSchemaShape re-validates the shape-constrained validations already written onto sch
// against sch's now-resolved type, stripping any that are illegal for that type and emitting
// CodeShapeMismatch for each (at pos).
//
// This exists for the top-level model decl path only: there the doc-comment block is dispatched
// (via DispatchSchemaLevel0) before the Go type has been built onto the schema, so the inline
// checkShape sees an empty type and cannot gate.
//
// Field- and items-level dispatch already run after their target's type is set, so they don't need
// it.
//
// uniqueItems is intentionally not rechecked: its grammar keyword (`unique`) carries no type-domain
// rule, matching the field/items paths which likewise never shape-gate it. diag may be nil.
// RecoerceDeclValues re-types `default`, `example` and `enum` on a declaration's schema once the Go
// type is known.
//
// A declaration's comment block is dispatched BEFORE its Go type is resolved onto the schema (see
// buildFromDecl), so these three keywords ran through ParseDefault / ParseEnumValues with an empty
// schema type and fell back to their raw string form: `default: 8080` on a named int became the
// string "8080", `enum: 1,2,3` became ["1","2","3"] — an enum no validator can satisfy on an
// integer schema — and a JSON array became a string holding JSON source.
//
// The same keywords on a struct field, a parameter or a header were always correct,
// because there the type is known when the walk runs.
func RecheckSchemaShape(sch *oaispec.Schema, pos token.Position, diag func(grammar.Diagnostic)) {
	if sch == nil || len(sch.Type) == 0 {
		return
	}
	typ := sch.Type[0]
	drop := func(keyword string, set bool, unset func()) {
		if !set {
			return
		}
		if ok, hint := validations.IsLegalForType(grammar.Keyword{Name: keyword}, typ); !ok {
			unset()
			if diag != nil {
				diag(grammar.Warnf(pos, grammar.CodeShapeMismatch, "%s", hint))
			}
		}
	}
	drop(grammar.KwPattern, sch.Pattern != "", func() { sch.Pattern = "" })
	drop(grammar.KwMinLength, sch.MinLength != nil, func() { sch.MinLength = nil })
	drop(grammar.KwMaxLength, sch.MaxLength != nil, func() { sch.MaxLength = nil })
	drop(grammar.KwMaximum, sch.Maximum != nil, func() { sch.Maximum = nil; sch.ExclusiveMaximum = false })
	drop(grammar.KwMinimum, sch.Minimum != nil, func() { sch.Minimum = nil; sch.ExclusiveMinimum = false })
	drop(grammar.KwMultipleOf, sch.MultipleOf != nil, func() { sch.MultipleOf = nil })
	drop(grammar.KwMinItems, sch.MinItems != nil, func() { sch.MinItems = nil })
	drop(grammar.KwMaxItems, sch.MaxItems != nil, func() { sch.MaxItems = nil })
	drop(grammar.KwMinProperties, sch.MinProperties != nil, func() { sch.MinProperties = nil })
	drop(grammar.KwMaxProperties, sch.MaxProperties != nil, func() { sch.MaxProperties = nil })
	drop(grammar.KwPatternProperties, len(sch.PatternProperties) > 0, func() { sch.PatternProperties = nil })
}

// clearStaleEnumDesc removes the x-go-enum-desc extension and strips the matching suffix from ps.Description.
//
// Called from SetEnum when a field-level `enum:` overrides a type-level `swagger:enum`.
//
// See [§stale-enum-desc](./README.md#stale-enum-desc).
func clearStaleEnumDesc(ps *oaispec.Schema) {
	enumDesc := resolvers.GetEnumDesc(ps.Extensions)
	if enumDesc == "" {
		return
	}
	delete(ps.Extensions, resolvers.ExtEnumDesc)
	ps.Description = strings.TrimSuffix(
		strings.TrimSuffix(ps.Description, enumDesc),
		"\n",
	)
}
