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

package generator

import (
	"errors"
	"fmt"
	"log"
	"maps"
	"os"
	"path"
	"path/filepath"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/go-openapi/analysis"
	"github.com/go-openapi/loads"
	"github.com/go-openapi/spec"
	"github.com/go-openapi/swag/jsonutils"
	"github.com/go-openapi/swag/mangling"
)

const asMethod = "()"

/*
Rewrite specification document first:

* anonymous objects
* tuples
* extensible objects (properties + additionalProperties)
* AllOfs when they match the rewrite criteria (not a nullable allOf)

Find string enums and generate specialized idiomatic enum with them

Every action that happens tracks the path which is a linked list of refs


*/

// GenerateModels generates all model files for some schema definitions.
func GenerateModels(modelNames []string, opts *GenOpts) error {
	// overide any default or incompatible options setting
	opts.IncludeModel = true
	opts.IgnoreOperations = true
	opts.ExistingModels = ""
	opts.IncludeHandler = false
	opts.IncludeMain = false
	opts.IncludeSupport = false
	generator, err := newAppGenerator("", modelNames, nil, opts)
	if err != nil {
		return err
	}

	return generator.Generate()
}

// GenerateDefinition generates a single model file for some schema definitions.
func GenerateDefinition(modelNames []string, opts *GenOpts) error {
	if err := opts.Prepare(); err != nil {
		return err
	}

	specDoc, _, err := newSpecAnalyzer(opts).analyzeSpec()
	if err != nil {
		return err
	}

	modelNames = pruneEmpty(modelNames)
	if len(modelNames) == 0 {
		for k := range specDoc.Spec().Definitions {
			modelNames = append(modelNames, k)
		}
	}

	for _, modelName := range modelNames {
		// lookup schema
		model, ok := specDoc.Spec().Definitions[modelName]
		if !ok {
			return fmt.Errorf("model %q not found in definitions given by %q", modelName, opts.Spec)
		}

		// generate files
		generator := definitionGenerator{
			Name:    modelName,
			Model:   model,
			SpecDoc: specDoc,
			Target: filepath.Join(
				opts.Target,
				filepath.FromSlash(opts.LanguageOpts.ManglePackagePath(opts.ModelPackage, ""))),
			opts: opts,
		}

		if err := generator.Generate(); err != nil {
			return err
		}
	}

	return nil
}

type definitionGenerator struct {
	Name    string
	Model   spec.Schema
	SpecDoc *loads.Document
	Target  string
	opts    *GenOpts
}

func (m *definitionGenerator) Generate() error {
	mod, err := makeGenDefinition(m.Name, m.Target, m.Model, m.SpecDoc, m.opts)
	if err != nil {
		return fmt.Errorf("could not generate definitions for model %s on target %s: %w", m.Name, m.Target, err)
	}

	if m.opts.DumpData {
		var dynamicMod any
		if err := jsonutils.FromDynamicJSON(mod, &dynamicMod); err != nil {
			return err
		}

		return dumpData(os.Stdout, dynamicMod)
	}

	if m.opts.IncludeModel {
		log.Println("including additional model")
		if err := m.generateModel(mod); err != nil {
			return fmt.Errorf("could not generate model: %w", err)
		}
	}
	log.Println("generated model", m.Name)

	return nil
}

func (m *definitionGenerator) generateModel(g *GenDefinition) error {
	return newRenderer(m.opts).renderDefinition(g)
}

func makeGenDefinition(name, pkg string, schema spec.Schema, specDoc *loads.Document, opts *GenOpts) (*GenDefinition, error) {
	gd, err := makeGenDefinitionHierarchy(name, pkg, "", schema, specDoc, opts)

	if err == nil && gd != nil {
		// before yielding the schema to the renderer, we check if the top-level Validate method gets some content
		// this means that the immediate content of the top level definitions has at least one validation.
		//
		// If none is found at this level and that no special case where no Validate() method is exposed at all
		// (e.g. io.ReadCloser and any types and their aliases), then there is an empty Validate() method which
		// just return nil (the object abides by the runtime.Validatable interface, but knows it has nothing to validate).
		//
		// We do this at the top level because of the possibility of aliased types which always bubble up validation to types which
		// are referring to them. This results in correct but inelegant code with empty validations.
		gd.HasValidations = shallowValidationLookup(gd.GenSchema)
	}
	return gd, err
}

//nolint:gocognit,gocyclo,cyclop // TODO(fredbi): refactor
func shallowValidationLookup(sch GenSchema) bool {
	// scan top level need for validations
	//
	// NOTE: this supersedes the previous NeedsValidation flag
	// With the introduction of this shallow lookup, it is no more necessary
	// to establish a distinction between HasValidations (e.g. carries on validations)
	// and NeedsValidation (e.g. should have a Validate method with something in it).
	// The latter was almost not used anyhow.

	if sch.HasAdditionalProperties && sch.AdditionalProperties == nil {
		log.Printf("warning: schema for additional properties in schema %q is empty. skipped", sch.Name)
	}

	if sch.IsArray && sch.HasValidations {
		return true
	}
	if sch.IsStream || sch.IsInterface { // these types have no validation - aliased types on those do not implement the Validatable interface
		return false
	}
	if sch.Required || hasFormatValidation(sch.resolvedType) {
		return true
	}
	if sch.HasStringValidations() || sch.HasNumberValidations() || sch.HasEnum() || len(sch.ItemsEnum) > 0 || sch.HasObjectValidations() {
		return true
	}
	for _, a := range sch.AllOf {
		if a.HasValidations {
			return true
		}
	}
	for _, p := range sch.Properties {
		// Using a base type within another structure triggers validation of the base type.
		// The discriminator property in the base type definition itself does not.
		if (p.HasValidations || p.Required) && (!sch.IsBaseType || p.Name != sch.DiscriminatorField) || (p.IsAliased || p.IsComplexObject) && !p.IsInterface && !p.IsStream {
			return true
		}
	}
	if sch.IsTuple && (sch.AdditionalItems != nil && (sch.AdditionalItems.HasValidations || sch.AdditionalItems.Required)) {
		return true
	}
	if sch.HasAdditionalProperties && sch.AdditionalProperties != nil && (sch.AdditionalProperties.IsInterface || sch.AdditionalProperties.IsStream) {
		return false
	}

	if sch.HasAdditionalProperties && sch.AdditionalProperties != nil && (sch.AdditionalProperties.HasValidations || sch.AdditionalProperties.Required || sch.AdditionalProperties.IsAliased && !sch.AdditionalProperties.IsInterface && !sch.AdditionalProperties.IsStream) {
		return true
	}

	if sch.IsAliased && (sch.IsPrimitive && sch.HasValidations) { // non primitive aliased have either other attributes with validation (above) or shall not validate
		return true
	}
	if sch.HasBaseType || sch.IsSubType {
		return true
	}
	return false
}

func isExternal(schema spec.Schema) bool {
	extType, ok := hasExternalType(schema.Extensions)
	return ok && !extType.Embedded
}

//nolint:gocognit // TODO(fredbi): refactor
func makeGenDefinitionHierarchy(name, pkg, container string, schema spec.Schema, specDoc *loads.Document, opts *GenOpts) (*GenDefinition, error) {
	// Check if model is imported from external package using x-go-type
	receiver := "m"
	// models are resolved in the current package
	modelPkg := opts.LanguageOpts.ManglePackageName(path.Base(filepath.ToSlash(pkg)), "definitions")
	resolver := newTypeResolver("", specDoc, opts).withDefinitionPackage(modelPkg)
	resolver.ModelName = name
	analyzed := analysis.New(specDoc.Spec())

	di := discriminatorInfo(analyzed, opts)

	pascalize, ok := opts.funcMap["pascalize"].(func(string) string)
	if !ok {
		return nil, errors.New("internal error: expected pascalize to be func(string) string")
	}

	jsonify, ok := opts.funcMap["json"].(func(any) (string, error))
	if !ok {
		return nil, errors.New("internal error: expected json to be func(any) (string, error)")
	}

	pg := schemaGenContext{
		Path:                       "",
		Name:                       name,
		Receiver:                   receiver,
		IndexVar:                   "i",
		ValueExpr:                  receiver,
		Schema:                     schema,
		Required:                   false,
		TypeResolver:               resolver,
		Named:                      true,
		ExtraSchemas:               make(map[string]GenSchema),
		Discrimination:             di,
		Container:                  container,
		IncludeValidator:           opts.IncludeValidator,
		IncludeModel:               opts.IncludeModel,
		StrictAdditionalProperties: opts.StrictAdditionalProperties,
		WithXML:                    opts.WithXML,
		StructTags:                 opts.StructTags,
		WantsRootedErrorPath:       opts.WantsRootedErrorPath,
		WantsStringer:              opts.WantsStringer,
		mangler:                    opts.LanguageOpts.Mangler,
		pascalize:                  pascalize,
		jsonify:                    jsonify,
	}

	if err := pg.makeGenSchema(); err != nil {
		return nil, fmt.Errorf("could not generate schema for %s: %w", name, err)
	}

	dsi, ok := di.Discriminators["#/definitions/"+name]
	if ok {
		// when these 2 are true then the schema will render as an interface
		pg.GenSchema.IsBaseType = true
		pg.GenSchema.IsExported = true
		pg.GenSchema.DiscriminatorField = dsi.FieldName

		if pg.GenSchema.Discriminates == nil {
			pg.GenSchema.Discriminates = make(map[string]string)
		}
		pg.GenSchema.Discriminates[name] = dsi.GoType
		pg.GenSchema.DiscriminatorValue = name

		for _, v := range dsi.Children {
			pg.GenSchema.Discriminates[v.FieldValue] = v.GoType
		}

		for j := range pg.GenSchema.Properties {
			if !strings.HasSuffix(pg.GenSchema.Properties[j].ValueExpression, asMethod) {
				pg.GenSchema.Properties[j].ValueExpression += asMethod
			}
		}
	}

	dse, ok := di.Discriminated["#/definitions/"+name]
	if ok {
		pg.GenSchema.DiscriminatorField = dse.FieldName
		pg.GenSchema.DiscriminatorValue = dse.FieldValue
		pg.GenSchema.IsSubType = true
		knownProperties := make(map[string]struct{})

		// find the referenced definitions
		// check if it has a discriminator defined
		// when it has a discriminator get the schema and run makeGenSchema for it.
		// replace the ref with this new genschema
		swsp := specDoc.Spec()
		for i, ss := range schema.AllOf {
			if pg.GenSchema.AllOf == nil {
				log.Printf("warning: resolved schema for subtype %q.AllOf[%d] is empty. skipped", name, i)
				continue
			}
			ref := ss.Ref
			for ref.String() != "" {
				var rsch *spec.Schema
				var err error
				rsch, err = spec.ResolveRef(swsp, &ref)
				if err != nil {
					return nil, err
				}
				if rsch != nil && rsch.Ref.String() != "" {
					ref = rsch.Ref
					continue
				}
				ref = spec.Ref{}
				if rsch != nil && rsch.Discriminator != "" {
					gs, err := makeGenDefinitionHierarchy(strings.TrimPrefix(ss.Ref.String(), "#/definitions/"), pkg, pg.GenSchema.Name, *rsch, specDoc, opts)
					if err != nil {
						return nil, err
					}
					gs.IsBaseType = true
					gs.IsExported = true
					pg.GenSchema.AllOf[i] = gs.GenSchema
					schPtr := &(pg.GenSchema.AllOf[i])
					if schPtr.AdditionalItems != nil {
						schPtr.AdditionalItems.IsBaseType = true
					}
					if schPtr.AdditionalProperties != nil {
						schPtr.AdditionalProperties.IsBaseType = true
					}
					for j := range schPtr.Properties {
						schPtr.Properties[j].IsBaseType = true
						knownProperties[schPtr.Properties[j].Name] = struct{}{}
					}
				}
			}
		}

		// dedupe the fields
		alreadySeen := make(map[string]struct{})
		for i, ss := range pg.GenSchema.AllOf {
			var remainingProperties GenSchemaList
			for _, p := range ss.Properties {
				if _, ok := knownProperties[p.Name]; !ok || ss.IsBaseType {
					if _, seen := alreadySeen[p.Name]; !seen {
						remainingProperties = append(remainingProperties, p)
						alreadySeen[p.Name] = struct{}{}
					}
				}
			}
			pg.GenSchema.AllOf[i].Properties = remainingProperties
		}
	}

	defaultImports := map[string]string{
		"errors":      "github.com/go-openapi/errors",
		"runtime":     "github.com/go-openapi/runtime",
		"strfmt":      "github.com/go-openapi/strfmt",
		"conv":        "github.com/go-openapi/swag/conv",
		"jsonutils":   "github.com/go-openapi/swag/jsonutils",
		"netutils":    "github.com/go-openapi/swag/netutils",
		"stringutils": "github.com/go-openapi/swag/stringutils",
		"typeutils":   "github.com/go-openapi/swag/typeutils",
		"validate":    "github.com/go-openapi/validate",
	}

	imports := findImports(&pg.GenSchema)
	if err := ensureDedupedImports(defaultImports, imports); err != nil {
		// guard against internal dev errors
		return nil, err
	}
	targetImport, err := opts.LanguageOpts.BaseImport(opts.Target)
	if err != nil {
		return nil, errTarget(opts.Target, err)
	}

	return &GenDefinition{
		GenCommon: GenCommon{
			Copyright:        opts.Copyright,
			TargetImportPath: targetImport,
		},
		Package:        modelPkg,
		CliPackage:     opts.CliPackage,
		GenSchema:      pg.GenSchema,
		DependsOn:      pg.Dependencies,
		DefaultImports: defaultImports,
		ExtraSchemas:   gatherExtraSchemas(pg.ExtraSchemas),
		Imports:        imports,
		External:       isExternal(schema),
	}, nil
}

func findImports(sch *GenSchema) map[string]string {
	imp := make(map[string]string, sensibleDefaultMapAlloc)
	t := sch.resolvedType
	if t.Pkg != "" && t.PkgAlias != "" {
		imp[t.PkgAlias] = t.Pkg
	}
	if t.IsEmbedded && t.ElemType != nil {
		if t.ElemType.Pkg != "" && t.ElemType.PkgAlias != "" {
			imp[t.ElemType.PkgAlias] = t.ElemType.Pkg
		}
	}
	if sch.Items != nil {
		sub := findImports(sch.Items)
		maps.Copy(imp, sub)
	}
	if sch.AdditionalItems != nil {
		sub := findImports(sch.AdditionalItems)
		maps.Copy(imp, sub)
	}
	if sch.Object != nil {
		sub := findImports(sch.Object)
		maps.Copy(imp, sub)
	}
	if sch.Properties != nil {
		for _, props := range sch.Properties {
			p := props
			sub := findImports(&p)
			maps.Copy(imp, sub)
		}
	}
	if sch.AdditionalProperties != nil {
		sub := findImports(sch.AdditionalProperties)
		maps.Copy(imp, sub)
	}
	if sch.AllOf != nil {
		for _, props := range sch.AllOf {
			p := props
			sub := findImports(&p)
			maps.Copy(imp, sub)
		}
	}
	for k, v := range sch.ExtraImports {
		if k != "" && v != "" {
			imp[k] = v
		}
	}

	return imp
}

type schemaGenContext struct {
	Required                   bool
	AdditionalProperty         bool
	Untyped                    bool
	Named                      bool
	IsVirtual                  bool
	IsTuple                    bool
	IncludeValidator           bool
	IncludeModel               bool
	StrictAdditionalProperties bool
	WantsRootedErrorPath       bool
	WantsStringer              bool
	WithXML                    bool
	Index                      int

	Path         string
	Name         string
	ParamName    string
	Accessor     string
	Receiver     string
	IndexVar     string
	KeyVar       string
	ValueExpr    string
	Container    string
	Schema       spec.Schema
	TypeResolver *typeResolver
	StructTags   []string

	GenSchema      GenSchema
	Dependencies   []string // NOTE: Dependencies is actually set nowhere
	ExtraSchemas   map[string]GenSchema
	Discriminator  *discor
	Discriminated  *discee
	Discrimination *discInfo

	// force to use container in inlined definitions (for deconflicting)
	UseContainerInName bool
	// indicates is the schema is part of a slice or a map
	IsElem bool
	// indicates is the schema is part of a struct
	IsProperty bool

	mangler   mangling.NameMangler
	pascalize func(string) string
	jsonify   func(any) (string, error)
}

func (sg *schemaGenContext) NewSliceBranch(schema *spec.Schema) *schemaGenContext {
	pg := sg.shallowClone()
	indexVar := pg.IndexVar
	if pg.Path == "" {
		pg.Path = "strconv.Itoa(" + indexVar + ")"
	} else {
		pg.Path = pg.Path + "+ \".\" + strconv.Itoa(" + indexVar + ")"
	}
	// check who is parent, if it's a base type then rewrite the value expression
	if sg.Discrimination != nil && sg.Discrimination.Discriminators != nil {
		_, rewriteValueExpr := sg.Discrimination.Discriminators["#/definitions/"+sg.TypeResolver.ModelName]
		if (pg.IndexVar == "i" && rewriteValueExpr) || sg.GenSchema.ElemType.IsBaseType {
			if !sg.GenSchema.IsAliased {
				pg.ValueExpr = sg.Receiver + "." + sg.mangler.ToJSONName(sg.GenSchema.Name) + "Field"
			} else {
				pg.ValueExpr = sg.Receiver
			}
		}
	}
	sg.GenSchema.IsBaseType = sg.GenSchema.ElemType.HasDiscriminator
	pg.IndexVar = indexVar + "i"
	pg.ValueExpr = pg.ValueExpr + "[" + indexVar + "]"
	pg.Schema = *schema
	pg.Required = false
	pg.IsElem = true
	if sg.IsVirtual {
		pg.TypeResolver = sg.TypeResolver.NewWithModelName(sg.TypeResolver.ModelName)
	}

	// when this is an anonymous complex object, this needs to become a ref
	return pg
}

func (sg *schemaGenContext) NewAdditionalItems(schema *spec.Schema) *schemaGenContext {
	pg := sg.shallowClone()
	indexVar := pg.IndexVar
	pg.Name = sg.Name + " items"
	itemsLen := 0
	if sg.Schema.Items != nil {
		itemsLen = sg.Schema.Items.Len()
	}
	var mod string
	if itemsLen > 0 {
		mod = "+" + strconv.Itoa(itemsLen)
	}
	if pg.Path == "" {
		pg.Path = "strconv.Itoa(" + indexVar + mod + ")"
	} else {
		pg.Path = pg.Path + "+ \".\" + strconv.Itoa(" + indexVar + mod + ")"
	}
	pg.IndexVar = indexVar
	pg.ValueExpr = sg.ValueExpr + "." + sg.GenSchema.GoName + "Items[" + indexVar + "]"
	pg.Schema = spec.Schema{}
	if schema != nil {
		pg.Schema = *schema
	}
	pg.Required = false
	return pg
}

func (sg *schemaGenContext) NewTupleElement(schema *spec.Schema, index int) *schemaGenContext {
	pg := sg.shallowClone()
	if pg.Path == "" {
		pg.Path = "\"" + strconv.Itoa(index) + "\""
	} else {
		pg.Path = pg.Path + "+ \".\"+\"" + strconv.Itoa(index) + "\""
	}
	pg.ValueExpr = pg.ValueExpr + ".P" + strconv.Itoa(index)

	pg.Required = true
	pg.IsTuple = true
	pg.Schema = *schema

	return pg
}

func (sg *schemaGenContext) NewStructBranch(name string, schema spec.Schema) *schemaGenContext {
	pg := sg.shallowClone()
	if sg.Path == "" {
		pg.Path = fmt.Sprintf("%q", name)
	} else {
		pg.Path = pg.Path + "+\".\"+" + fmt.Sprintf("%q", name)
	}
	pg.Name = name
	pg.ValueExpr = pg.ValueExpr + "." + schemaGoName(&schema, name, sg.mangler)
	pg.Schema = schema
	pg.IsProperty = true
	if slices.Contains(sg.Schema.Required, name) {
		pg.Required = true
	}
	return pg
}

func (sg *schemaGenContext) NewCompositionBranch(schema spec.Schema, index int) *schemaGenContext {
	pg := sg.shallowClone()
	pg.Schema = schema
	pg.Name = "AO" + strconv.Itoa(index)
	if sg.Name != sg.TypeResolver.ModelName {
		pg.Name = sg.Name + pg.Name
	}
	pg.Index = index
	return pg
}

func (sg *schemaGenContext) NewAdditionalProperty(schema spec.Schema) *schemaGenContext {
	pg := sg.shallowClone()
	pg.Schema = schema
	if pg.KeyVar == "" {
		pg.ValueExpr = sg.ValueExpr
	}
	pg.KeyVar += "k"
	pg.ValueExpr += "[" + pg.KeyVar + "]"
	pg.Path = pg.KeyVar
	pg.GenSchema.Suffix = "Value"
	if sg.Path != "" {
		pg.Path = sg.Path + "+\".\"+" + pg.KeyVar
	}
	pg.IsElem = true
	// propagates the special IsNullable override for maps of slices and
	// maps of aliased types.
	pg.GenSchema.IsMapNullOverride = sg.GenSchema.IsMapNullOverride
	return pg
}

func (sg *schemaGenContext) MergeResult(other *schemaGenContext, liftsRequired bool) {
	sg.GenSchema.HasValidations = sg.GenSchema.HasValidations || mergeValidation(other)
	sg.GenSchema.HasContextValidations = sg.GenSchema.HasContextValidations || other.GenSchema.HasContextValidations

	if liftsRequired && other.GenSchema.AdditionalProperties != nil && other.GenSchema.AdditionalProperties.Required {
		sg.GenSchema.Required = true
	}
	if liftsRequired && other.GenSchema.Required {
		sg.GenSchema.Required = other.GenSchema.Required
	}

	if other.GenSchema.HasBaseType {
		sg.GenSchema.HasBaseType = other.GenSchema.HasBaseType
	}

	sg.Dependencies = append(sg.Dependencies, other.Dependencies...)

	// lift extra schemas
	maps.Copy(sg.ExtraSchemas, other.ExtraSchemas)
	if other.GenSchema.IsMapNullOverride {
		sg.GenSchema.IsMapNullOverride = true
	}

	// lift extra imports
	if other.GenSchema.Pkg != "" && other.GenSchema.PkgAlias != "" {
		sg.GenSchema.ExtraImports[other.GenSchema.PkgAlias] = other.GenSchema.Pkg
	}
	maps.Copy(sg.GenSchema.ExtraImports, other.GenSchema.ExtraImports)
}

func (sg *schemaGenContext) GoName() string {
	return goName(&sg.Schema, sg.Name)
}

func (sg *schemaGenContext) shallowClone() *schemaGenContext {
	pg := new(schemaGenContext)
	*pg = *sg
	if pg.Container == "" {
		pg.Container = sg.Name
	}
	pg.GenSchema = GenSchema{StructTags: sg.StructTags}
	pg.Dependencies = nil
	pg.Named = false
	pg.Index = 0
	pg.IsTuple = false
	pg.IncludeValidator = sg.IncludeValidator
	pg.IncludeModel = sg.IncludeModel
	pg.StrictAdditionalProperties = sg.StrictAdditionalProperties
	return pg
}

func (sg *schemaGenContext) schemaValidations() sharedValidations {
	model := sg.Schema

	isRequired := sg.Required
	if model.Default != nil || model.ReadOnly {
		// when readOnly or default is specified, this disables Required validation (Swagger-specific)
		isRequired = false
		if sg.Required {
			log.Printf("warning: properties with a default value or readOnly should not be required [%s]", sg.Name)
		}
	}

	v := model.Validations()
	return sharedValidations{
		Required:            sg.Required, /* TODO(fred): guard for cases with discriminator field, default and readOnly*/
		SchemaValidations:   v,
		HasSliceValidations: v.HasArrayValidations() || v.HasEnum(),
		HasValidations:      hasValidations(&model, isRequired),
	}
}

//nolint:gocognit,gocyclo,cyclop,maintidx // TODO(fredbi): refactor
func (sg *schemaGenContext) buildProperties() error {
	for k, v := range sg.Schema.Properties {
		vv := v

		// check if this requires de-anonymizing, if so lift this as a new struct and extra schema
		tpe, err := sg.TypeResolver.ResolveSchema(&vv, true, sg.IsTuple || slices.Contains(sg.Schema.Required, k))
		if err != nil {
			return err
		}
		if sg.Schema.Discriminator == k {
			tpe.IsNullable = false
		}

		var hasValidation bool
		if tpe.IsComplexObject && tpe.IsAnonymous && len(v.Properties) > 0 {
			// this is an anonymous complex construct: build a new type for it
			pg := sg.makeNewStruct(sg.makeRefName()+sg.mangler.ToGoName(k), v)
			pg.IsTuple = sg.IsTuple
			if sg.Path != "" {
				pg.Path = sg.Path + "+ \".\"+" + fmt.Sprintf("%q", k)
			} else {
				pg.Path = fmt.Sprintf("%q", k)
			}
			if err := pg.makeGenSchema(); err != nil {
				return err
			}
			if v.Discriminator != "" {
				pg.GenSchema.IsBaseType = true
				pg.GenSchema.IsExported = true
				pg.GenSchema.HasBaseType = true
			}

			vv = *spec.RefProperty("#/definitions/" + pg.Name)
			hasValidation = pg.GenSchema.HasValidations
			sg.ExtraSchemas[pg.Name] = pg.GenSchema
			// NOTE: MergeResult lifts validation status and extra schemas
			sg.MergeResult(pg, false)
		}

		emprop := sg.NewStructBranch(k, vv)
		emprop.IsTuple = sg.IsTuple

		if err := emprop.makeGenSchema(); err != nil {
			return err
		}

		// whatever the validations says, if we have an any, do not validate
		// NOTE: this may be the case when the type is left empty and we get a Enum validation.
		if emprop.GenSchema.IsInterface || emprop.GenSchema.IsStream {
			emprop.GenSchema.HasValidations = false
		} else if hasValidation || emprop.GenSchema.HasValidations || emprop.GenSchema.Required || emprop.GenSchema.IsAliased || len(emprop.GenSchema.AllOf) > 0 {
			emprop.GenSchema.HasValidations = true
			sg.GenSchema.HasValidations = true
		}

		// generates format validation on property
		emprop.GenSchema.HasValidations = emprop.GenSchema.HasValidations || hasFormatValidation(tpe)

		if emprop.Schema.Ref.String() != "" {
			// expand the schema of this property, so we take informed decisions about its type
			ref := emprop.Schema.Ref
			var sch *spec.Schema
			for ref.String() != "" {
				var rsch *spec.Schema
				var err error
				specDoc := sg.TypeResolver.Doc
				rsch, err = spec.ResolveRef(specDoc.Spec(), &ref)
				if err != nil {
					return err
				}
				if rsch == nil {
					return errors.New("spec.ResolveRef returned nil schema")
				}
				if rsch.Ref.String() != "" {
					ref = rsch.Ref
					continue
				}
				ref = spec.Ref{}
				sch = rsch
			}

			if emprop.Discrimination != nil {
				if _, ok := emprop.Discrimination.Discriminators[emprop.Schema.Ref.String()]; ok {
					emprop.GenSchema.IsBaseType = true
					emprop.GenSchema.IsNullable = false
					emprop.GenSchema.HasBaseType = true
				}
				if _, ok := emprop.Discrimination.Discriminated[emprop.Schema.Ref.String()]; ok {
					emprop.GenSchema.IsSubType = true
				}
			}

			// set property name
			nm := filepath.Base(emprop.Schema.Ref.GetURL().Fragment)

			tr := sg.TypeResolver.NewWithModelName(goName(&emprop.Schema, sg.mangler.ToGoName(nm)))
			ttpe, err := tr.ResolveSchema(sch, false, true)
			if err != nil {
				return err
			}
			if ttpe.IsAliased {
				emprop.GenSchema.IsAliased = true
			}

			// lift validations
			hv := hasValidations(sch, false)

			// include format validation, excluding binary
			hv = hv || hasFormatValidation(ttpe)

			// a base type property is always validated against the base type
			// exception: for the base type definition itself (see shallowValidationLookup())
			if (hv || emprop.GenSchema.IsBaseType) && !emprop.GenSchema.IsInterface && !emprop.GenSchema.IsStream {
				emprop.GenSchema.HasValidations = true
			}
			if ttpe.HasAdditionalItems && sch.AdditionalItems.Schema != nil {
				// when AdditionalItems specifies a Schema, there is a validation
				// check if we stepped upon an exception
				child, err := tr.ResolveSchema(sch.AdditionalItems.Schema, false, true)
				if err != nil {
					return err
				}
				if !child.IsInterface && !child.IsStream {
					emprop.GenSchema.HasValidations = true
				}
			}
			if ttpe.IsMap && sch.AdditionalProperties != nil && sch.AdditionalProperties.Schema != nil {
				// when AdditionalProperties specifies a Schema, there is a validation
				// check if we stepped upon an exception
				child, err := tr.ResolveSchema(sch.AdditionalProperties.Schema, false, true)
				if err != nil {
					return err
				}
				if !child.IsInterface && !child.IsStream {
					emprop.GenSchema.HasValidations = true
				}
			}
		}

		if sg.Schema.Discriminator == k {
			// this is the discriminator property:
			// it is required, but forced as non-nullable,
			// since we never fill it with a zero-value
			// TODO: when no other property than discriminator, there is no validation
			emprop.GenSchema.IsNullable = false
		}
		if emprop.GenSchema.IsBaseType {
			sg.GenSchema.HasBaseType = true
		}
		sg.MergeResult(emprop, false)

		// when discriminated, data is accessed via a getter func
		if emprop.GenSchema.HasDiscriminator {
			emprop.GenSchema.ValueExpression += asMethod
		}

		emprop.GenSchema.Extensions = emprop.Schema.Extensions

		// set custom serializer tag
		if customTag, found := tpe.Extensions[xGoCustomTag]; found {
			tagAsStr, ok := customTag.(string)
			if ok {
				emprop.GenSchema.CustomTag = tagAsStr
			} else {
				log.Printf("warning: expect %s extension to be a string, got: %v. Skipped", xGoCustomTag, customTag)
			}
		}
		sg.GenSchema.Properties = append(sg.GenSchema.Properties, emprop.GenSchema)
	}
	sort.Sort(sg.GenSchema.Properties)

	return nil
}

//nolint:gocognit // TODO(fredbi): refactor
func (sg *schemaGenContext) buildAllOf() error {
	if len(sg.Schema.AllOf) == 0 {
		return nil
	}

	var hasArray, hasNonArray int

	sort.Sort(sg.GenSchema.AllOf)
	if sg.Container == "" {
		sg.Container = sg.Name
	}
	for i, schema := range sg.Schema.AllOf {
		sch := schema
		tpe, ert := sg.TypeResolver.ResolveSchema(&sch, sch.Ref.String() == "", false)
		if ert != nil {
			return ert
		}

		// check for multiple arrays in allOf branches.
		// Although a valid JSON-Schema construct, it is not suited for serialization.
		// This is the same if we attempt to serialize an array with another object.
		// We issue a generation warning on this.
		if tpe.IsArray {
			hasArray++
		} else {
			hasNonArray++
		}
		if (tpe.IsAnonymous && len(sch.AllOf) > 0) || (sch.Ref.String() == "" && !tpe.IsComplexObject && (tpe.IsArray || tpe.IsInterface || tpe.IsPrimitive)) {
			// cases where anonymous structures cause the creation of a new type:
			// - nested allOf: this one is itself a AllOf: build a new type for it
			// - anonymous simple types for edge cases: array, primitive, any
			// NOTE: when branches are aliased or anonymous, the nullable property in the branch type is lost.
			name := sg.mangler.ToVarName(goName(&sch, sg.makeRefName()+"AllOf"+strconv.Itoa(i)))
			ng := sg.makeNewStruct(name, sch)
			if err := ng.makeGenSchema(); err != nil {
				return err
			}

			newsch := spec.RefProperty("#/definitions/" + ng.Name)
			sg.Schema.AllOf[i] = *newsch

			pg := sg.NewCompositionBranch(*newsch, i)
			if err := pg.makeGenSchema(); err != nil {
				return err
			}

			// lift extra schemas & validations from new type
			pg.MergeResult(ng, true)

			// lift validations when complex or ref'ed:
			// - parent always calls its Validatable child
			// - child may or may not have validations
			//
			// Exception: child is not Validatable when interface or stream
			if !pg.GenSchema.IsInterface && !pg.GenSchema.IsStream {
				sg.GenSchema.HasValidations = true
			}

			// add the newly created type to the list of schemas to be rendered inline
			pg.ExtraSchemas[ng.Name] = ng.GenSchema

			sg.MergeResult(pg, true)

			sg.GenSchema.AllOf = append(sg.GenSchema.AllOf, pg.GenSchema)

			continue
		}

		comprop := sg.NewCompositionBranch(sch, i)
		if err := comprop.makeGenSchema(); err != nil {
			return err
		}
		if comprop.GenSchema.IsMap && comprop.GenSchema.HasAdditionalProperties && comprop.GenSchema.AdditionalProperties != nil && !comprop.GenSchema.IsInterface {
			// the anonymous branch is a map for AdditionalProperties: rewrite value expression
			comprop.GenSchema.ValueExpression = comprop.GenSchema.ValueExpression + "." + comprop.GenSchema.GoName
			comprop.GenSchema.AdditionalProperties.ValueExpression = comprop.GenSchema.ValueExpression + "[" + comprop.GenSchema.AdditionalProperties.KeyVar + "]"
		}

		// lift validations when complex or ref'ed
		if (comprop.GenSchema.IsComplexObject || comprop.Schema.Ref.String() != "") && !comprop.GenSchema.IsInterface && !comprop.GenSchema.IsStream {
			comprop.GenSchema.HasValidations = true
		}
		sg.MergeResult(comprop, true)
		sg.GenSchema.AllOf = append(sg.GenSchema.AllOf, comprop.GenSchema)
	}

	if hasArray > 1 || (hasArray > 0 && hasNonArray > 0) {
		log.Printf("warning: cannot generate serializable allOf with conflicting array definitions in %s", sg.Container)
	}

	// AllOf types are always considered nullable, except when an extension says otherwise
	if override, ok := sg.TypeResolver.isNullableOverride(&sg.Schema); ok {
		sg.GenSchema.IsNullable = override
	} else {
		sg.GenSchema.IsNullable = true
	}

	// prevent IsAliased to bubble up (e.g. when a single branch is itself aliased)
	const boundConsideredSingleBranch = 2
	sg.GenSchema.IsAliased = sg.GenSchema.IsAliased && len(sg.GenSchema.AllOf) < boundConsideredSingleBranch

	return nil
}

//nolint:gocognit,gocyclo,cyclop,maintidx // TODO(fredbi): refactor
func (sg *schemaGenContext) buildAdditionalProperties() error {
	if sg.Schema.AdditionalProperties == nil {
		if sg.Schema.MinProperties == nil && sg.Schema.MaxProperties == nil {
			return nil
		}

		// whenever there is a validation on min/max properties and no additionalProperties is defined,
		// we imply additionalProperties: true (corresponds to jsonschema defaults).
		sg.Schema.AdditionalProperties = &spec.SchemaOrBool{Allows: true}
	}
	addp := *sg.Schema.AdditionalProperties

	wantsAdditional := addp.Schema != nil || addp.Allows
	sg.GenSchema.HasAdditionalProperties = wantsAdditional
	if !wantsAdditional {
		return nil
	}

	// flag swap
	if sg.GenSchema.IsComplexObject {
		sg.GenSchema.IsAdditionalProperties = true
		sg.GenSchema.IsComplexObject = false
		sg.GenSchema.IsMap = false
	}

	if addp.Schema == nil {
		// this is for AdditionalProperties:true|false
		if addp.Allows {
			// additionalProperties: true is rendered as: map[string]any
			addp.Schema = &spec.Schema{}

			addp.Schema.Typed("object", "")
			sg.GenSchema.HasAdditionalProperties = true
			sg.GenSchema.IsComplexObject = false
			sg.GenSchema.IsMap = true

			if !sg.IsElem && !sg.IsProperty {
				sg.GenSchema.ValueExpression += "." + sg.mangler.ToGoName(sg.Name+" additionalProperties")
			}
			cp := sg.NewAdditionalProperty(*addp.Schema)
			cp.Name += "AdditionalProperties"
			cp.Required = false
			if err := cp.makeGenSchema(); err != nil {
				return err
			}
			sg.MergeResult(cp, false)
			sg.GenSchema.AdditionalProperties = &cp.GenSchema
		}
		return nil
	}

	if !sg.GenSchema.IsMap && (sg.GenSchema.IsAdditionalProperties && sg.Named) {
		// we have a complex object with an AdditionalProperties schema

		tpe, ert := sg.TypeResolver.ResolveSchema(addp.Schema, addp.Schema.Ref.String() == "", false)
		if ert != nil {
			return ert
		}

		if tpe.IsComplexObject && tpe.IsAnonymous {
			// if the AdditionalProperties is an anonymous complex object, generate a new type for it
			pg := sg.makeNewStruct(sg.makeRefName()+" Anon", *addp.Schema)
			if err := pg.makeGenSchema(); err != nil {
				return err
			}
			sg.MergeResult(pg, false)
			sg.ExtraSchemas[pg.Name] = pg.GenSchema

			sg.Schema.AdditionalProperties.Schema = spec.RefProperty("#/definitions/" + pg.Name)
			sg.IsVirtual = true

			comprop := sg.NewAdditionalProperty(*sg.Schema.AdditionalProperties.Schema)
			if err := comprop.makeGenSchema(); err != nil {
				return err
			}

			comprop.GenSchema.Required = true
			comprop.GenSchema.HasValidations = true

			comprop.GenSchema.ValueExpression = sg.GenSchema.ValueExpression + "." + sg.GenSchema.GoName + "[" + comprop.KeyVar + "]"

			sg.GenSchema.AdditionalProperties = &comprop.GenSchema
			sg.GenSchema.HasAdditionalProperties = true
			sg.GenSchema.ValueExpression += "." + sg.GenSchema.GoName

			sg.MergeResult(comprop, false)

			return nil
		}

		// this is a regular named schema for AdditionalProperties
		sg.GenSchema.ValueExpression += "." + sg.GenSchema.GoName
		comprop := sg.NewAdditionalProperty(*addp.Schema)
		d := sg.TypeResolver.Doc
		asch, err := analysis.Schema(analysis.SchemaOpts{
			Root:     d.Spec(),
			BasePath: d.SpecFilePath(),
			Schema:   addp.Schema,
		})
		if err != nil {
			return err
		}
		comprop.Required = !asch.IsSimpleSchema && !asch.IsMap
		if err := comprop.makeGenSchema(); err != nil {
			return err
		}
		sg.MergeResult(comprop, false)
		sg.GenSchema.AdditionalProperties = &comprop.GenSchema
		sg.GenSchema.AdditionalProperties.ValueExpression = sg.GenSchema.ValueExpression + "[" + comprop.KeyVar + "]"

		// rewrite value expression for arrays and arrays of arrays in maps (rendered as map[string][][]...)
		if sg.GenSchema.AdditionalProperties.IsArray {
			// maps of slices are where an override may take effect
			if sg.GenSchema.AdditionalProperties.Items == nil {
				return fmt.Errorf("items schema not defined for additional property in %q", sg.Name)
			}
			sg.GenSchema.AdditionalProperties.Items.IsMapNullOverride = sg.GenSchema.AdditionalProperties.IsMapNullOverride
			sg.GenSchema.AdditionalProperties.Items.ValueExpression = sg.GenSchema.ValueExpression + "[" + comprop.KeyVar + "]" + "[" + sg.GenSchema.AdditionalProperties.IndexVar + "]"
			ap := sg.GenSchema.AdditionalProperties.Items
			for ap != nil && ap.IsArray {
				ap.Items.IsMapNullOverride = ap.IsMapNullOverride
				ap.Items.ValueExpression = ap.ValueExpression + "[" + ap.IndexVar + "]"
				ap = ap.Items
			}
		}

		// lift validation
		if (sg.GenSchema.AdditionalProperties.IsComplexObject || sg.GenSchema.AdditionalProperties.IsAliased || sg.GenSchema.AdditionalProperties.Required) && !sg.GenSchema.AdditionalProperties.IsInterface && !sg.GenSchema.IsStream {
			sg.GenSchema.HasValidations = true
		}
		return nil
	}

	if sg.GenSchema.IsMap && wantsAdditional {
		// this is itself an AdditionalProperties schema with some AdditionalProperties.
		// this also runs for aliased map types (with zero properties save additionalProperties)
		//
		// find out how deep this rabbit hole goes
		// descend, unwind and rewrite
		// This needs to be depth first, so it first goes as deep as it can and then
		// builds the result in reverse order.
		_, ls, err := newMapStack(sg)
		if err != nil {
			return err
		}
		return ls.Build()
	}

	if sg.GenSchema.IsAdditionalProperties && !sg.Named {
		// for an anonymous object, first build the new object
		// and then replace the current one with a $ref to the
		// new object
		newObj := sg.makeNewStruct(sg.GenSchema.Name+" P"+strconv.Itoa(sg.Index), sg.Schema)
		if err := newObj.makeGenSchema(); err != nil {
			return err
		}

		hasMapNullOverride := sg.GenSchema.IsMapNullOverride
		sg.GenSchema = GenSchema{StructTags: sg.StructTags}
		sg.Schema = *spec.RefProperty("#/definitions/" + newObj.Name)
		if err := sg.makeGenSchema(); err != nil {
			return err
		}
		sg.MergeResult(newObj, false)

		sg.GenSchema.IsMapNullOverride = hasMapNullOverride
		if sg.GenSchema.IsArray {
			sg.GenSchema.Items.IsMapNullOverride = hasMapNullOverride
		}

		sg.GenSchema.HasValidations = newObj.GenSchema.HasValidations
		sg.ExtraSchemas[newObj.Name] = newObj.GenSchema
		return nil
	}
	return nil
}

func (sg *schemaGenContext) makeNewStruct(name string, schema spec.Schema) *schemaGenContext {
	sp := sg.TypeResolver.Doc.Spec()
	name = sg.mangler.ToGoName(name)
	if sg.TypeResolver.ModelName != sg.Name {
		name = sg.mangler.ToGoName(sg.TypeResolver.ModelName + " " + name)
	}
	if sp.Definitions == nil {
		sp.Definitions = make(spec.Definitions)
	}
	sp.Definitions[name] = schema
	pg := schemaGenContext{
		Path:                       "",
		Name:                       name,
		Receiver:                   sg.Receiver,
		IndexVar:                   "i",
		ValueExpr:                  sg.Receiver,
		Schema:                     schema,
		Required:                   false,
		Named:                      true,
		ExtraSchemas:               make(map[string]GenSchema),
		Discrimination:             sg.Discrimination,
		Container:                  sg.Container,
		IncludeValidator:           sg.IncludeValidator,
		IncludeModel:               sg.IncludeModel,
		StrictAdditionalProperties: sg.StrictAdditionalProperties,
		StructTags:                 sg.StructTags,
		mangler:                    sg.mangler,
		pascalize:                  sg.pascalize,
		jsonify:                    sg.jsonify,
	}
	if schema.Ref.String() == "" {
		pg.TypeResolver = sg.TypeResolver.NewWithModelName(name)
	}
	pg.GenSchema.IsVirtual = true

	sg.ExtraSchemas[name] = pg.GenSchema
	return &pg
}

func (sg *schemaGenContext) buildArray() error {
	tpe, err := sg.TypeResolver.ResolveSchema(sg.Schema.Items.Schema, true, false)
	if err != nil {
		return err
	}

	// check if the element is a complex object, if so generate a new type for it
	if tpe.IsComplexObject && tpe.IsAnonymous {
		pg := sg.makeNewStruct(sg.makeRefName()+" items"+strconv.Itoa(sg.Index), *sg.Schema.Items.Schema)
		if err := pg.makeGenSchema(); err != nil {
			return err
		}
		sg.MergeResult(pg, false)
		sg.ExtraSchemas[pg.Name] = pg.GenSchema
		sg.Schema.Items.Schema = spec.RefProperty("#/definitions/" + pg.Name)
		sg.IsVirtual = true
		return sg.makeGenSchema()
	}

	// create the generation schema for items
	elProp := sg.NewSliceBranch(sg.Schema.Items.Schema)

	// when building a slice of maps, the map item is not required
	// items from maps of aliased or nullable type remain required

	// NOTE(fredbi): since this is reset below, this Required = true serves the obscure purpose
	// of indirectly lifting validations from the slice. This is carried out differently now.
	// elProp.Required = true

	if err := elProp.makeGenSchema(); err != nil {
		return err
	}

	sg.MergeResult(elProp, false)

	sg.GenSchema.IsBaseType = elProp.GenSchema.IsBaseType
	sg.GenSchema.ItemsEnum = elProp.GenSchema.Enum
	elProp.GenSchema.Suffix = "Items"

	elProp.GenSchema.IsNullable = tpe.IsNullable && !tpe.HasDiscriminator
	if elProp.GenSchema.IsNullable {
		sg.GenSchema.GoType = "[]*" + elProp.GenSchema.GoType
	} else {
		sg.GenSchema.GoType = "[]" + elProp.GenSchema.GoType
	}

	sg.GenSchema.IsArray = true

	schemaCopy := elProp.GenSchema

	schemaCopy.Required = false

	// validations of items
	// include format validation, excluding binary and base64 format validation
	hv := hasValidations(sg.Schema.Items.Schema, false) || hasFormatValidation(schemaCopy.resolvedType)

	// base types of polymorphic types must be validated
	// NOTE: IsNullable is not useful to figure out a validation: we use Refed and IsAliased below instead
	if hv || elProp.GenSchema.IsBaseType {
		schemaCopy.HasValidations = true
	}

	if (elProp.Schema.Ref.String() != "" || elProp.GenSchema.IsAliased) && !elProp.GenSchema.IsInterface && !elProp.GenSchema.IsStream {
		schemaCopy.HasValidations = true
	}

	// lift validations
	sg.GenSchema.HasValidations = sg.GenSchema.HasValidations || schemaCopy.HasValidations
	sg.GenSchema.HasSliceValidations = sg.Schema.Validations().HasArrayValidations() || sg.Schema.Validations().HasEnum()

	// prevents bubbling custom formatter flag
	sg.GenSchema.IsCustomFormatter = false

	sg.GenSchema.Items = &schemaCopy
	if sg.Named {
		sg.GenSchema.AliasedType = sg.GenSchema.GoType
	}

	return nil
}

//nolint:gocognit // TODO(fredbi): refactor
func (sg *schemaGenContext) buildItems() error {
	if sg.Schema.Items == nil {
		// in swagger, arrays MUST have an items schema
		return nil
	}

	// in Items spec, we have either Schema (array) or Schemas (tuple)
	presentsAsSingle := sg.Schema.Items.Schema != nil
	if presentsAsSingle && sg.Schema.AdditionalItems != nil { // unsure if this a valid of invalid schema
		return fmt.Errorf("single schema (%s) can't have additional items", sg.Name)
	}
	if presentsAsSingle {
		return sg.buildArray()
	}

	// This is a tuple, build a new model that represents this
	if sg.Named {
		sg.GenSchema.Name = sg.Name
		sg.GenSchema.GoType = sg.TypeResolver.goTypeName(sg.Name)
		for i, sch := range sg.Schema.Items.Schemas {
			s := sch
			elProp := sg.NewTupleElement(&s, i)

			if s.Ref.String() == "" {
				tpe, err := sg.TypeResolver.ResolveSchema(&s, s.Ref.String() == "", true)
				if err != nil {
					return err
				}
				if tpe.IsComplexObject && tpe.IsAnonymous {
					// if the tuple element is an anonymous complex object, build a new type for it
					pg := sg.makeNewStruct(sg.makeRefName()+" Items"+strconv.Itoa(i), s)
					if err := pg.makeGenSchema(); err != nil {
						return err
					}
					elProp.Schema = *spec.RefProperty("#/definitions/" + pg.Name)
					elProp.MergeResult(pg, false)
					elProp.ExtraSchemas[pg.Name] = pg.GenSchema
				}
			}

			if err := elProp.makeGenSchema(); err != nil {
				return err
			}
			if elProp.GenSchema.IsInterface || elProp.GenSchema.IsStream {
				elProp.GenSchema.HasValidations = false
			}
			sg.MergeResult(elProp, false)

			elProp.GenSchema.Name = "p" + strconv.Itoa(i)
			elProp.GenSchema.GoName = exportGoName("P"+strconv.Itoa(i), false, sg.mangler)
			sg.GenSchema.Properties = append(sg.GenSchema.Properties, elProp.GenSchema)
			sg.GenSchema.IsTuple = true
		}
		return nil
	}

	// for an anonymous object, first build the new object
	// and then replace the current one with a $ref to the
	// new tuple object
	var sch spec.Schema
	sch.Typed("object", "")
	sch.Properties = make(map[string]spec.Schema, len(sg.Schema.Items.Schemas))
	for i, v := range sg.Schema.Items.Schemas {
		sch.Required = append(sch.Required, "P"+strconv.Itoa(i))
		sch.Properties["P"+strconv.Itoa(i)] = v
	}
	sch.AdditionalItems = sg.Schema.AdditionalItems
	tup := sg.makeNewStruct(sg.GenSchema.Name+"Tuple"+strconv.Itoa(sg.Index), sch)
	tup.IsTuple = true
	if err := tup.makeGenSchema(); err != nil {
		return err
	}
	tup.GenSchema.IsTuple = true
	tup.GenSchema.IsComplexObject = false
	tup.GenSchema.Title = tup.GenSchema.Name + " a representation of an anonymous Tuple type"
	tup.GenSchema.Description = ""
	sg.ExtraSchemas[tup.Name] = tup.GenSchema

	sg.Schema = *spec.RefProperty("#/definitions/" + tup.Name)
	if err := sg.makeGenSchema(); err != nil {
		return err
	}
	sg.MergeResult(tup, false)
	return nil
}

func (sg *schemaGenContext) buildAdditionalItems() error {
	wantsAdditionalItems := sg.Schema.AdditionalItems != nil &&
		(sg.Schema.AdditionalItems.Allows || sg.Schema.AdditionalItems.Schema != nil)

	sg.GenSchema.HasAdditionalItems = wantsAdditionalItems
	if wantsAdditionalItems {
		// check if the element is a complex object, if so generate a new type for it
		tpe, err := sg.TypeResolver.ResolveSchema(sg.Schema.AdditionalItems.Schema, true, true)
		if err != nil {
			return err
		}
		if tpe.IsComplexObject && tpe.IsAnonymous {
			pg := sg.makeNewStruct(sg.makeRefName()+" Items", *sg.Schema.AdditionalItems.Schema)
			if err := pg.makeGenSchema(); err != nil {
				return err
			}
			sg.Schema.AdditionalItems.Schema = spec.RefProperty("#/definitions/" + pg.Name)
			pg.GenSchema.HasValidations = true
			sg.MergeResult(pg, false)
			sg.ExtraSchemas[pg.Name] = pg.GenSchema
		}

		it := sg.NewAdditionalItems(sg.Schema.AdditionalItems.Schema)
		// if AdditionalItems are themselves arrays, bump the index var
		if tpe.IsArray {
			it.IndexVar += "i"
		}

		if tpe.IsInterface {
			it.Untyped = true
		}

		if err := it.makeGenSchema(); err != nil {
			return err
		}

		// lift validations when complex is not anonymous or ref'ed
		if (tpe.IsComplexObject || it.Schema.Ref.String() != "") && !tpe.IsInterface && !tpe.IsStream {
			it.GenSchema.HasValidations = true
		}

		sg.MergeResult(it, true)
		sg.GenSchema.AdditionalItems = &it.GenSchema
	}
	return nil
}

func (sg *schemaGenContext) buildXMLNameWithTags() error {
	// render some "xml" struct tag under one the following conditions:
	// - consumes/produces in spec contains xml
	// - struct tags CLI option contains xml
	// - XML object present in spec for this schema
	if sg.WithXML || slices.Contains(sg.StructTags, "xml") || sg.Schema.XML != nil {
		sg.GenSchema.XMLName = sg.Name

		if sg.Schema.XML != nil {
			if sg.Schema.XML.Name != "" {
				sg.GenSchema.XMLName = sg.Schema.XML.Name
			}
			if sg.Schema.XML.Attribute {
				sg.GenSchema.XMLName += ",attr"
			}
		}
	}
	return nil
}

func (sg *schemaGenContext) shortCircuitNamedRef() (bool, error) {
	// This if block ensures that a struct gets
	// rendered with the ref as embedded ref.
	//
	// NOTE: this assumes that all $ref point to a definition,
	// i.e. the spec is canonical, as guaranteed by minimal flattening.
	//
	if !sg.Named || sg.Schema.Ref.String() == "" {
		return false, nil
	}

	// Simple aliased types (arrays, maps and primitives)
	//
	// Before deciding to make a struct with a composition branch (below),
	// check if the $ref points to a simple type or polymorphic (base) type.
	//
	// If this is the case, just realias this simple type, without creating a struct.
	//
	// In templates this case is identified by .IsSuperAlias = true
	asch, era := analysis.Schema(analysis.SchemaOpts{
		Root:     sg.TypeResolver.Doc.Spec(),
		BasePath: sg.TypeResolver.Doc.SpecFilePath(),
		Schema:   &sg.Schema,
	})
	if era != nil {
		return false, era
	}

	if asch.IsArray || asch.IsMap || asch.IsKnownType || asch.IsBaseType {
		tpx, ers := sg.TypeResolver.ResolveSchema(&sg.Schema, false, true)
		if ers != nil {
			return false, ers
		}
		tpe := resolvedType{}
		tpe.IsMap = asch.IsMap
		tpe.IsArray = asch.IsArray
		tpe.IsPrimitive = asch.IsKnownType

		tpe.IsAliased = true
		tpe.AliasedType = ""
		tpe.IsComplexObject = false
		tpe.IsAnonymous = false
		tpe.IsCustomFormatter = false
		tpe.IsBaseType = tpx.IsBaseType

		tpe.GoType = sg.TypeResolver.goTypeName(path.Base(sg.Schema.Ref.String()))
		tpe.Pkg = sg.TypeResolver.definitionPkg

		tpe.IsNullable = tpx.IsNullable // TODO
		tpe.IsInterface = tpx.IsInterface
		tpe.IsStream = tpx.IsStream
		tpe.IsEmbedded = tpx.IsEmbedded

		tpe.SwaggerType = tpx.SwaggerType
		sch := spec.Schema{}
		pg := sg.makeNewStruct(sg.Name, sch)
		if err := pg.makeGenSchema(); err != nil {
			return true, err
		}
		sg.MergeResult(pg, true)
		sg.GenSchema = pg.GenSchema
		sg.GenSchema.resolvedType = tpe
		sg.GenSchema.IsSuperAlias = true
		sg.GenSchema.IsBaseType = tpe.IsBaseType

		return true, nil
	}

	// Aliased object: use golang struct composition.
	// Covers case of a type redefinition like:
	// thistype:
	//   $ref: #/definitions/othertype
	//
	// This is rendered as a struct with type field, i.e. :
	// Alias struct {
	//		AliasedType
	// }
	//
	// In templates, the schema is composed like AllOf.
	nullableOverride := sg.GenSchema.IsNullable

	tpe := resolvedType{}
	tpe.GoType = sg.TypeResolver.goTypeName(sg.Name)
	tpe.Pkg = sg.TypeResolver.definitionPkg
	tpe.SwaggerType = "object"
	tpe.IsComplexObject = true
	tpe.IsMap = false
	tpe.IsArray = false
	tpe.IsAnonymous = false
	tpe.IsNullable = sg.TypeResolver.isNullable(&sg.Schema)

	branch := sg.NewCompositionBranch(sg.Schema, 0)
	if err := branch.makeGenSchema(); err != nil {
		return true, err
	}
	sg.GenSchema.resolvedType = tpe
	sg.GenSchema.IsNullable = sg.GenSchema.IsNullable || nullableOverride
	// prevent format from bubbling up in composed type
	branch.GenSchema.IsCustomFormatter = false

	sg.MergeResult(branch, true)

	tpx, ers := sg.TypeResolver.ResolveSchema(&sg.Schema, false, true)
	if ers != nil {
		return false, ers
	}
	// we don't know the actual validation status yet. So assume true,
	// unless we can infer that no Validate() method will be present
	branch.GenSchema.HasValidations = !tpx.IsInterface && !tpx.IsStream
	sg.GenSchema.AllOf = append(sg.GenSchema.AllOf, branch.GenSchema)

	return true, nil
}

// liftSpecialAllOf attempts to simplify the rendering of allOf constructs by lifting simple things into the current schema.
func (sg *schemaGenContext) liftSpecialAllOf() error {
	// if there is only a $ref or a primitive and an x-isnullable schema then this is a nullable pointer
	// so this should not compose several objects, just 1
	// if there is a ref with a discriminator then we look for x-class on the current definition to know
	// the value of the discriminator to instantiate the class
	const boundConsideredSingleBranch = 2
	if len(sg.Schema.AllOf) < boundConsideredSingleBranch {
		return nil
	}
	var seenSchema int
	var seenNullable bool
	var schemaToLift spec.Schema

	for _, schema := range sg.Schema.AllOf {
		sch := schema
		tpe, err := sg.TypeResolver.ResolveSchema(&sch, true, true)
		if err != nil {
			return err
		}
		if sg.TypeResolver.isNullable(&sch) {
			seenNullable = true
		}
		if len(sch.Type) > 0 || len(sch.Properties) > 0 || sch.Ref.GetURL() != nil || len(sch.AllOf) > 0 {
			seenSchema++
			if seenSchema > 1 {
				// won't do anything if several candidates for a lift
				break
			}
			if (!tpe.IsAnonymous && tpe.IsComplexObject) || tpe.IsPrimitive {
				// lifting complex objects here results in inlined structs in the model
				schemaToLift = sch
			}
		}
	}

	if seenSchema == 1 {
		// when there only a single schema to lift in allOf, replace the schema by its allOf definition
		sg.Schema = schemaToLift
		sg.GenSchema.IsNullable = seenNullable
	}
	return nil
}

func (sg *schemaGenContext) buildAliased() error {
	if !sg.GenSchema.IsPrimitive && !sg.GenSchema.IsMap && !sg.GenSchema.IsArray && !sg.GenSchema.IsInterface {
		return nil
	}

	if sg.GenSchema.IsPrimitive {
		if sg.GenSchema.SwaggerType == "string" && sg.GenSchema.SwaggerFormat == "" {
			sg.GenSchema.IsAliased = sg.GenSchema.GoType != sg.GenSchema.SwaggerType
		}
		if sg.GenSchema.IsNullable && sg.Named {
			sg.GenSchema.IsNullable = false
		}
	}

	if sg.GenSchema.IsInterface {
		sg.GenSchema.IsAliased = sg.GenSchema.GoType != iface
	}

	if sg.GenSchema.IsMap {
		sg.GenSchema.IsAliased = !strings.HasPrefix(sg.GenSchema.GoType, "map[")
	}

	if sg.GenSchema.IsArray {
		sg.GenSchema.IsAliased = !strings.HasPrefix(sg.GenSchema.GoType, "[]")
	}
	return nil
}

func (sg schemaGenContext) makeRefName() string {
	// figure out a longer name for deconflicting anonymous models.
	// This is used when makeNewStruct() is followed by the creation of a new ref to definitions
	if sg.UseContainerInName && sg.Container != sg.Name {
		return sg.Container + sg.mangler.ToGoName(sg.Name)
	}
	return sg.Name
}

func (sg *schemaGenContext) derefMapElement(outer *GenSchema, _ *GenSchema, elem *GenSchema) {
	derefType := strings.TrimPrefix(elem.GoType, "*")

	if outer.IsAliased {
		nesting := strings.TrimSuffix(strings.TrimSuffix(outer.AliasedType, elem.GoType), "*")
		outer.AliasedType = nesting + derefType
		outer.GoType = derefType
	} else {
		nesting := strings.TrimSuffix(strings.TrimSuffix(outer.GoType, elem.GoType), "*")
		outer.GoType = nesting + derefType
	}

	elem.GoType = derefType
}

func (sg *schemaGenContext) checkNeedsPointer(outer *GenSchema, sch *GenSchema, elem *GenSchema) {
	derefType := strings.TrimPrefix(elem.GoType, "*")
	switch {
	case outer.IsAliased && !strings.HasSuffix(outer.AliasedType, "*"+derefType):
		// override nullability of map of primitive elements: render element of aliased or anonymous map as a pointer
		outer.AliasedType = strings.TrimSuffix(outer.AliasedType, derefType) + "*" + derefType
	case sch != nil:
		// nullable primitive
		if sch.IsAnonymous && !strings.HasSuffix(outer.GoType, "*"+derefType) {
			sch.GoType = strings.TrimSuffix(sch.GoType, derefType) + "*" + derefType
		}
	case outer.IsAnonymous && !strings.HasSuffix(outer.GoType, "*"+derefType):
		outer.GoType = strings.TrimSuffix(outer.GoType, derefType) + "*" + derefType
	}
}

// buildMapOfNullable equalizes the nullablity status for aliased and anonymous maps of simple things,
// with the nullability of its innermost element.
//
// NOTE: at the moment, we decide to align the type of the outer element (map) to the type of the inner element
// The opposite could be done and result in non nullable primitive elements. If we do so, the validation
// code needs to be adapted by removing IsZero() and Required() calls in codegen.
//
//nolint:gocognit // TODO(fredbi): refactor
func (sg *schemaGenContext) buildMapOfNullable(sch *GenSchema) {
	outer := &sg.GenSchema
	if sch == nil {
		sch = outer
	}
	if sch.IsMap && (outer.IsAliased || outer.IsAnonymous) {
		elem := sch.AdditionalProperties
		for elem != nil {
			if elem.IsPrimitive && elem.IsNullable {
				sg.checkNeedsPointer(outer, nil, elem)
			} else if elem.IsArray {
				// override nullability of array of primitive elements:
				// render element of aliased or anonyous map as a pointer
				it := elem.Items
				for it != nil {
					switch {
					case it.IsPrimitive && it.IsNullable:
						sg.checkNeedsPointer(outer, sch, it)
					case it.IsMap:
						sg.buildMapOfNullable(it)
					case !it.IsPrimitive && !it.IsArray && it.IsComplexObject && it.IsNullable:
						// structs in map are not rendered as pointer by default
						// unless some x-nullable overrides says so
						_, forced := it.Extensions[xNullable]
						if !forced {
							_, forced = it.Extensions[xIsNullable]
						}
						if !forced {
							sg.derefMapElement(outer, sch, it)
						}
					}
					it = it.Items
				}
			}
			elem = elem.AdditionalProperties
		}
	}
}

//nolint:gocognit,gocyclo,cyclop,maintidx // TODO(fredbi): refactor
func (sg *schemaGenContext) makeGenSchema() error {
	sg.GenSchema.Example = ""
	if sg.Schema.Example != nil {
		data, err := sg.jsonify(sg.Schema.Example)
		if err != nil {
			return err
		}
		// Deleting the unnecessary double quotes for string types
		// otherwise the generate spec will generate as "\"foo\""
		sg.GenSchema.Example = strings.Trim(data, "\"")
	}
	sg.GenSchema.ExternalDocs = trimExternalDoc(sg.Schema.ExternalDocs)
	sg.GenSchema.IsExported = true
	sg.GenSchema.Path = sg.Path
	sg.GenSchema.IndexVar = sg.IndexVar
	sg.GenSchema.Location = body
	sg.GenSchema.KeyVar = sg.KeyVar
	sg.GenSchema.OriginalName = sg.Name
	sg.GenSchema.Name = sg.GoName()
	sg.GenSchema.Title = sg.Schema.Title
	sg.GenSchema.Description = trimBOM(sg.Schema.Description)
	sg.GenSchema.ReceiverName = sg.Receiver
	sg.GenSchema.sharedValidations = sg.schemaValidations()
	sg.GenSchema.ReadOnly = sg.Schema.ReadOnly
	sg.GenSchema.IncludeValidator = sg.IncludeValidator
	sg.GenSchema.IncludeModel = sg.IncludeModel
	sg.GenSchema.StrictAdditionalProperties = sg.StrictAdditionalProperties
	sg.GenSchema.Default = sg.Schema.Default
	sg.GenSchema.StructTags = sg.StructTags
	sg.GenSchema.ExtraImports = make(map[string]string)
	sg.GenSchema.WantsRootedErrorPath = sg.WantsRootedErrorPath
	sg.GenSchema.WantsStringer = sg.WantsStringer
	sg.GenSchema.IsElem = sg.IsElem
	sg.GenSchema.IsProperty = sg.IsProperty
	sg.GenSchema.GoName = schemaGoName(&sg.Schema, sg.Name, sg.mangler)
	sg.GenSchema.ValueExpression = sg.ValueExpr

	var err error
	returns, err := sg.shortCircuitNamedRef()
	if err != nil {
		return err
	}
	if returns {
		// short circuited on a resolved $ref
		return nil
	}

	if e := sg.liftSpecialAllOf(); e != nil {
		return e
	}
	nullableOverride := sg.GenSchema.IsNullable

	if sg.Container == "" {
		sg.Container = sg.GenSchema.Name
	}
	if e := sg.buildAllOf(); e != nil {
		return e
	}

	var tpe resolvedType
	if sg.Untyped {
		tpe, err = sg.TypeResolver.ResolveSchema(nil, !sg.Named, sg.IsTuple || sg.Required || sg.GenSchema.Required)
	} else {
		tpe, err = sg.TypeResolver.ResolveSchema(&sg.Schema, !sg.Named, sg.IsTuple || sg.Required || sg.GenSchema.Required)
	}
	if err != nil {
		return err
	}

	tpe.IsNullable = tpe.IsNullable || nullableOverride
	sg.GenSchema.resolvedType = tpe
	sg.GenSchema.IsBaseType = tpe.IsBaseType
	sg.GenSchema.HasDiscriminator = tpe.HasDiscriminator

	// include format validations, excluding binary
	sg.GenSchema.HasValidations = sg.GenSchema.HasValidations || hasFormatValidation(tpe)

	// include context validations
	sg.GenSchema.HasContextValidations = sg.GenSchema.HasContextValidations || hasContextValidations(&sg.Schema) && !tpe.IsInterface && !tpe.IsStream && !tpe.SkipExternalValidation

	// usage of a polymorphic base type is rendered with getter funcs on private properties.
	// In the case of aliased types, the value expression remains unchanged to the receiver.
	if tpe.IsArray && tpe.ElemType != nil && tpe.ElemType.IsBaseType && sg.GenSchema.ValueExpression != sg.GenSchema.ReceiverName {
		sg.GenSchema.ValueExpression += asMethod
	}

	if tpe.IsExternal { // anonymous external types
		extType, pkg, alias := sg.TypeResolver.knownDefGoType(sg.GenSchema.Name, sg.Schema, sg.TypeResolver.goTypeName)
		if pkg != "" && alias != "" {
			sg.GenSchema.ExtraImports[alias] = pkg
		}

		if !tpe.IsEmbedded {
			sg.GenSchema.resolvedType = tpe
			sg.GenSchema.Required = sg.Required
			// assume we validate everything but interface and io.Reader - validation may be disabled by using the noValidation hint
			sg.GenSchema.HasValidations = !tpe.IsInterface && !tpe.IsStream && !tpe.SkipExternalValidation
			sg.GenSchema.IsAliased = sg.GenSchema.HasValidations

			log.Printf("INFO: type %s is external, with inferred spec type %s, referred to as %s", sg.GenSchema.Name, sg.GenSchema.GoType, extType)
			sg.GenSchema.GoType = extType
			sg.GenSchema.AliasedType = extType

			// short circuit schema building for external types
			return nil
		}
		// TODO: case for embedded types as anonymous definitions
		return errors.New("ERROR: inline definitions embedded types are not supported")
	}

	if e := sg.buildAdditionalProperties(); e != nil {
		return e
	}

	// rewrite value expression from top-down
	cur := &sg.GenSchema
	for cur.AdditionalProperties != nil {
		cur.AdditionalProperties.ValueExpression = cur.ValueExpression + "[" + cur.AdditionalProperties.KeyVar + "]"
		cur = cur.AdditionalProperties
	}

	prev := sg.GenSchema
	if sg.Untyped {
		tpe, err = sg.TypeResolver.ResolveSchema(nil, !sg.Named, sg.Named || sg.IsTuple || sg.Required || sg.GenSchema.Required)
	} else {
		tpe, err = sg.TypeResolver.ResolveSchema(&sg.Schema, !sg.Named, sg.Named || sg.IsTuple || sg.Required || sg.GenSchema.Required)
	}
	if err != nil {
		return err
	}
	tpe.IsNullable = tpe.IsNullable || nullableOverride
	sg.GenSchema.resolvedType = tpe
	sg.GenSchema.IsComplexObject = prev.IsComplexObject
	sg.GenSchema.IsMap = prev.IsMap
	sg.GenSchema.IsAdditionalProperties = prev.IsAdditionalProperties
	sg.GenSchema.IsBaseType = sg.GenSchema.HasDiscriminator
	sg.GenSchema.IsElem = prev.IsElem
	sg.GenSchema.IsProperty = prev.IsProperty

	if err := sg.buildProperties(); err != nil {
		return err
	}

	if err := sg.buildXMLNameWithTags(); err != nil {
		return err
	}

	if err := sg.buildAdditionalItems(); err != nil {
		return err
	}

	if err := sg.buildItems(); err != nil {
		return err
	}

	if err := sg.buildAliased(); err != nil {
		return err
	}

	sg.buildMapOfNullable(nil)

	// extra serializers & interfaces

	// generate MarshalBinary for:
	// - tuple
	// - struct
	// - map
	// - aliased primitive of a formatter type which is not a stringer
	//
	// but not for:
	// - any
	// - io.Reader
	gs := sg.GenSchema
	sg.GenSchema.WantsMarshalBinary = !gs.IsInterface && !gs.IsStream && !gs.IsBaseType &&
		(gs.IsTuple || gs.IsComplexObject || gs.IsAdditionalProperties || (gs.IsPrimitive && gs.IsAliased && gs.IsCustomFormatter && !strings.Contains(gs.Zero(), `("`)))

	// generate a fmt.Stringer String() method when the --with-stringer option is set.
	// This opt-in renders the model field values (as JSON) rather than the Go syntax
	// representation, so that pointer properties show their value, not their address.
	// Restricted to the struct/tuple/map shapes that use a pointer receiver, mirroring
	// MarshalBinary (see issue #872).
	sg.GenSchema.WantsStringer = sg.WantsStringer && !gs.IsInterface && !gs.IsStream && !gs.IsBaseType &&
		(gs.IsTuple || gs.IsComplexObject || gs.IsAdditionalProperties)

	return nil
}

func goName(sch *spec.Schema, orig string) string {
	name, _ := sch.Extensions.GetString(xGoName)
	if name != "" {
		return name
	}
	return orig
}

func hasContextValidations(model *spec.Schema) bool {
	// always assume ref needs context validate
	// TODO: find away to determine ref needs context validate or not
	if model.ReadOnly || model.Ref.String() != "" {
		return true
	}
	return false
}

func hasValidations(model *spec.Schema, isRequired bool) bool {
	if isRequired {
		return true
	}

	v := model.Validations()
	if v.HasNumberValidations() || v.HasStringValidations() || v.HasArrayValidations() || v.HasEnum() || v.HasObjectValidations() {
		return true
	}

	// since this was added to deal with discriminator, we'll fix this when testing discriminated types
	if len(model.Properties) > 0 && model.Discriminator == "" {
		return true
	}

	// lift validations from allOf branches
	for _, s := range model.AllOf {
		schema := s
		if s.Ref.String() != "" || hasValidations(&schema, false) {
			return true
		}
	}

	return false
}

func hasFormatValidation(tpe resolvedType) bool {
	if tpe.IsCustomFormatter && !tpe.IsStream && !tpe.IsBase64 {
		return true
	}
	if tpe.IsArray && tpe.ElemType != nil {
		return hasFormatValidation(*tpe.ElemType)
	}
	return false
}

func mergeValidation(other *schemaGenContext) bool {
	// NOTE: NeesRequired and NeedsValidation are deprecated
	if other.GenSchema.AdditionalProperties != nil && other.GenSchema.AdditionalProperties.HasValidations {
		return true
	}
	if other.GenSchema.AdditionalItems != nil && other.GenSchema.AdditionalItems.HasValidations {
		return true
	}
	for _, sch := range other.GenSchema.AllOf {
		if sch.HasValidations {
			return true
		}
	}
	return other.GenSchema.HasValidations
}
