// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package buf

// This is an adapted version

import (
	"fmt"
	"math"
	"slices"
	"strings"
	"unicode"

	"buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
	"github.com/bufbuild/protovalidate-go/resolve"
	"google.golang.org/protobuf/reflect/protoreflect"
)

// An enumeration of the JSON Schema type names.
const (
	jsArray   = "array"
	jsBoolean = "boolean"
	jsInteger = "integer"
	jsNull    = "null"
	jsNumber  = "number"
	jsObject  = "object"
	jsString  = "string"
)

type FieldVisibility int

const (
	FieldVisible FieldVisibility = iota
	FieldHide
	FieldIgnore
)

type GeneratorOption func(*jsonSchemaGenerator)

// WithJSONNames sets the generator to use JSON field names as the primary name.
func WithJSONNames() GeneratorOption {
	return func(p *jsonSchemaGenerator) {
		p.useJSONNames = true
	}
}

// Generate generates a JSON schema for the given message descriptor, with protobuf field names.
func Generate(input protoreflect.MessageDescriptor, opts ...GeneratorOption) map[string]interface{} {
	generator := &jsonSchemaGenerator{}
	generator.custom = generator.makeWktGenerators()
	for _, opt := range opts {
		opt(generator)
	}
	schema := map[string]interface{}{}
	generator.generate(input, schema)
	return schema
}

type jsonSchemaGenerator struct {
	custom       map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{})
	useJSONNames bool
}

func (p *jsonSchemaGenerator) generate(desc protoreflect.MessageDescriptor, schema map[string]interface{}) {
	if custom, ok := p.custom[desc.FullName()]; ok { // Custom generator.
		custom(desc, nil, schema)
	} else { // Default generator.
		p.generateDefault(desc, schema)
	}
}

func (p *jsonSchemaGenerator) generateDefault(desc protoreflect.MessageDescriptor, schema map[string]interface{}) {
	schema["type"] = jsObject
	p.setDescription(desc, schema)
	var required []string
	properties := make(map[string]interface{})
	patternProperties := make(map[string]interface{})
	for i := range desc.Fields().Len() {
		field := desc.Fields().Get(i)
		visibility := p.shouldIgnoreField(field)
		if visibility == FieldIgnore {
			continue
		}
		constraints := p.getFieldConstraints(field)
		if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_UNPOPULATED {
			required = append(required, string(field.Name()))
		}

		// Generate the schema.
		fieldSchema := p.generateField(field, constraints)

		// TODO: Add an option to include custom alias.
		aliases := make([]string, 0, 1)

		switch {
		case visibility == FieldHide:
			aliases = append(aliases, string(field.Name()))
			if field.JSONName() != string(field.Name()) {
				aliases = append(aliases, field.JSONName())
			}
		case p.useJSONNames:
			// Use the JSON name as the primary name.
			properties[field.JSONName()] = fieldSchema
			if field.JSONName() != string(field.Name()) {
				aliases = append(aliases, string(field.Name()))
			}
		default:
			// Use the proto name as the primary name.
			properties[string(field.Name())] = fieldSchema
			if field.JSONName() != string(field.Name()) {
				aliases = append(aliases, field.JSONName())
			}
		}

		if len(aliases) > 0 {
			pattern := "^(" + strings.Join(aliases, "|") + ")$"
			patternProperties[pattern] = fieldSchema
		}
	}
	schema["properties"] = properties
	schema["additionalProperties"] = false
	if len(patternProperties) > 0 {
		schema["patternProperties"] = patternProperties
	}
	if len(required) > 0 {
		schema["required"] = required
	}
}

func (p *jsonSchemaGenerator) setDescription(desc protoreflect.Descriptor, schema map[string]interface{}) {
	src := desc.ParentFile().SourceLocations().ByDescriptor(desc)
	var descriptions []string
	if src.LeadingComments != "" {
		descriptions = append(descriptions, strings.TrimSpace(src.LeadingComments))
	}
	if src.TrailingComments != "" {
		descriptions = append(descriptions, strings.TrimSpace(src.TrailingComments))
	}
	if len(descriptions) > 0 {
		schema["description"] = strings.Join(descriptions, " | ")
	}
}

func (p *jsonSchemaGenerator) generateField(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints) map[string]interface{} {
	var schema = make(map[string]interface{})
	p.setDescription(field, schema)
	p.generateValidation(field, constraints, schema)
	return schema
}

func (p *jsonSchemaGenerator) generateValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	if field.IsList() {
		schema["type"] = jsArray
		items := make(map[string]interface{})
		schema["items"] = items
		schema = items
		constraints = constraints.GetRepeated().GetItems()
	}
	switch field.Kind() {
	case protoreflect.BoolKind:
		p.generateBoolValidation(field, constraints, schema)
	case protoreflect.EnumKind:
		p.generateEnumValidation(field, constraints, schema)
	case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
		p.generateInt32Validation(field, constraints, schema)
	case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
		p.generateInt64Validation(field, constraints, schema)
	case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
		p.generateUint32Validation(field, constraints, schema)
	case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
		p.generateUint64Validation(field, constraints, schema)
	case protoreflect.FloatKind:
		p.generateFloatValidation(field, constraints, schema, 32)
	case protoreflect.DoubleKind:
		p.generateFloatValidation(field, constraints, schema, 64)
	case protoreflect.StringKind:
		p.generateStringValidation(field, constraints, schema)
	case protoreflect.BytesKind:
		p.generateBytesValidation(field, constraints, schema)
	case protoreflect.MessageKind, protoreflect.GroupKind:
		if field.IsMap() {
			schema["type"] = jsObject
			propertyNames := make(map[string]interface{})
			constraints := p.getFieldConstraints(field)
			p.generateValidation(field.MapKey(), constraints.GetMap().GetKeys(), propertyNames)
			schema["propertyNames"] = propertyNames
			properties := make(map[string]interface{})
			p.generateValidation(field.MapValue(), constraints.GetMap().GetValues(), properties)
			schema["additionalProperties"] = properties
		} else {
			p.generateMessageValidation(field, schema)
		}
	}
}

func (p *jsonSchemaGenerator) getFieldConstraints(field protoreflect.FieldDescriptor) *validate.FieldConstraints {
	constraints := resolve.FieldConstraints(field)
	if constraints == nil || constraints.GetIgnore() == validate.Ignore_IGNORE_ALWAYS {
		return nil
	}
	return constraints
}

func (p *jsonSchemaGenerator) generateBoolValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	schema["type"] = jsBoolean
	if !field.HasPresence() && constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE {
		// False is not allowed.
		schema["enum"] = []bool{true}
	} else if constraints.GetBool() != nil && constraints.GetBool().Const != nil {
		schema["enum"] = []bool{constraints.GetBool().GetConst()}
	}
}

func generateTitle(name protoreflect.Name) string {
	// Convert camel case to space separated words.
	var result strings.Builder
	for i, chr := range name {
		isUpper := unicode.IsUpper(chr)
		nextIsUpper := i+1 >= len(name) || unicode.IsUpper(rune(name[i+1]))
		if i > 0 && isUpper && !nextIsUpper {
			result.WriteRune(' ')
		}
		result.WriteRune(chr)
	}
	return result.String()
}

type enumFieldSelector struct {
	selected bool
	index    int
}

func (p *jsonSchemaGenerator) generateEnumValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	enumFieldSelectors := make(map[int32]enumFieldSelector, field.Enum().Values().Len())
	for i := range field.Enum().Values().Len() {
		val := field.Enum().Values().Get(i)
		enumFieldSelectors[int32(val.Number())] = enumFieldSelector{
			selected: true,
			index:    i,
		}
	}

	if constraints.GetEnum() != nil && constraints.GetEnum().HasConst() {
		for number := range enumFieldSelectors {
			if number != constraints.GetEnum().GetConst() {
				enumFieldSelectors[number] = enumFieldSelector{}
			}
		}
	}

	if constraints.GetEnum() != nil && len(constraints.GetEnum().In) > 0 {
		inMap := make(map[int32]struct{}, len(constraints.GetEnum().In))
		for _, value := range constraints.GetEnum().In {
			inMap[value] = struct{}{}
		}

		for number := range enumFieldSelectors {
			if _, ok := inMap[number]; !ok {
				enumFieldSelectors[number] = enumFieldSelector{}
			}
		}
	}

	if constraints.GetEnum() != nil && len(constraints.GetEnum().NotIn) > 0 {
		for _, value := range constraints.GetEnum().NotIn {
			enumFieldSelectors[value] = enumFieldSelector{}
		}
	}

	onlySelectIntValues := constraints.GetEnum() != nil &&
		(constraints.GetEnum().GetDefinedOnly() ||
			constraints.GetEnum().HasConst() ||
			constraints.GetEnum().GetIn() != nil)

	validIntegers := map[string]interface{}{"type": jsInteger, "minimum": math.MinInt32, "maximum": math.MaxInt32}
	if onlySelectIntValues {
		var integerValues = make([]int32, 0)
		for number, val := range enumFieldSelectors {
			if val.selected {
				integerValues = append(integerValues, number)
			}
		}
		slices.Sort(integerValues)

		validIntegers = map[string]interface{}{"type": jsInteger, "enum": integerValues}
	}

	validIndexes := make([]int, 0, len(enumFieldSelectors))
	for _, val := range enumFieldSelectors {
		if val.selected {
			validIndexes = append(validIndexes, val.index)
		}
	}
	slices.Sort(validIndexes)

	var stringValues = make([]string, 0)
	for _, index := range validIndexes {
		stringValues = append(stringValues, string(field.Enum().Values().Get(index).Name()))
	}

	validStrings := map[string]interface{}{"type": jsString, "enum": stringValues, "title": generateTitle(field.Enum().Name())}

	schema["anyOf"] = []map[string]interface{}{validStrings, validIntegers}
}

type baseRule[T comparable] interface {
	HasConst() bool
	GetConst() T
	GetIn() []T
}

type numberRule[T comparable] interface {
	baseRule[T]

	HasGte() bool
	GetGte() T
	HasGt() bool
	GetGt() T
	HasLte() bool
	GetLte() T
	HasLt() bool
	GetLt() T
}

func generateConstInValidation[T comparable](constraints baseRule[T], schema map[string]interface{}) {
	if constraints.HasConst() {
		schema["enum"] = []T{constraints.GetConst()}
	} else if len(constraints.GetIn()) > 0 {
		schema["enum"] = constraints.GetIn()
	}
}

func generateIntValidation[T int32 | int64](
	constraints numberRule[T],
	bits int,
	schema map[string]interface{},
) {
	numberSchema := map[string]interface{}{
		"type": jsInteger,
	}
	minVal := -(1 << (bits - 1))
	maxExclVal := uint64(1) << (bits - 1)
	var orNumberSchema map[string]interface{}

	generateConstInValidation(constraints, numberSchema)
	switch {
	case constraints.HasGt():
		var isOr bool
		switch {
		case constraints.HasLt():
			isOr = constraints.GetLt() <= constraints.GetGt()
		case constraints.HasLte():
			isOr = constraints.GetLte() <= constraints.GetGt()
		}
		if isOr {
			orNumberSchema = map[string]interface{}{"exclusiveMinimum": constraints.GetGt()}
		} else {
			numberSchema["exclusiveMinimum"] = constraints.GetGt()
		}
	case constraints.HasGte():
		var isOr bool
		switch {
		case constraints.HasLt():
			isOr = constraints.GetLt() <= constraints.GetGte()
		case constraints.HasLte():
			isOr = constraints.GetLte() < constraints.GetGte()
		}
		if isOr {
			orNumberSchema = map[string]interface{}{"minimum": constraints.GetGte()}
		} else {
			numberSchema["minimum"] = constraints.GetGte()
		}
	default:
		numberSchema["minimum"] = minVal
	}
	switch {
	case constraints.HasLt():
		numberSchema["exclusiveMaximum"] = constraints.GetLt()
	case constraints.HasLte():
		numberSchema["maximum"] = constraints.GetLte()
	default:
		numberSchema["exclusiveMaximum"] = maxExclVal
	}

	anyOf := []map[string]interface{}{
		numberSchema,
	}

	if orNumberSchema != nil {
		numberSchema["minimum"] = minVal
		orNumberSchema["exclusiveMaximum"] = maxExclVal
		orNumberSchema["type"] = jsInteger
		anyOf = append(anyOf, orNumberSchema)
	}
	if bits > 52 {
		anyOf = append(anyOf, map[string]interface{}{
			"type":    jsString,
			"pattern": "^-?[0-9]+$",
		})
	}
	if len(anyOf) > 1 {
		schema["anyOf"] = anyOf
	} else {
		for key, value := range numberSchema {
			schema[key] = value
		}
	}
}

func (p *jsonSchemaGenerator) generateInt32Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	switch {
	default:
		schema["type"] = jsInteger
		schema["minimum"] = math.MinInt32
		schema["maximum"] = math.MaxInt32
	case constraints.GetInt32() != nil:
		generateIntValidation(constraints.GetInt32(), 32, schema)
	case constraints.GetSint32() != nil:
		generateIntValidation(constraints.GetSint32(), 32, schema)
	case constraints.GetSfixed32() != nil:
		generateIntValidation(constraints.GetSfixed32(), 32, schema)
	}
}

func (p *jsonSchemaGenerator) generateInt64Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	switch {
	default:
		schema["anyOf"] = []map[string]interface{}{
			{"type": jsInteger, "minimum": math.MinInt64, "exclusiveMaximum": math.Pow(2, 63)},
			{"type": jsString, "pattern": "^-?[0-9]+$"},
		}
	case constraints.GetInt64() != nil:
		generateIntValidation(constraints.GetInt64(), 64, schema)
	case constraints.GetSint64() != nil:
		generateIntValidation(constraints.GetSint64(), 64, schema)
	case constraints.GetSfixed64() != nil:
		generateIntValidation(constraints.GetSfixed64(), 64, schema)
	}
}

func generateUintValidation[T uint32 | uint64](
	constraints numberRule[T],
	bits int,
	schema map[string]interface{},
) {
	numberSchema := map[string]interface{}{
		"type": jsInteger,
	}
	var orNumberSchema map[string]interface{}
	maxExclVal := float64(uint64(1)<<(bits-1)) * 2
	generateConstInValidation(constraints, numberSchema)
	switch {
	case constraints.HasGt():
		var isOr bool
		switch {
		case constraints.HasLt():
			isOr = constraints.GetLt() <= constraints.GetGt()
		case constraints.HasLte():
			isOr = constraints.GetLte() <= constraints.GetGt()
		}
		if isOr {
			orNumberSchema = map[string]interface{}{"exclusiveMinimum": constraints.GetGt()}
		} else {
			numberSchema["exclusiveMinimum"] = constraints.GetGt()
		}
	case constraints.HasGte():
		var isOr bool
		switch {
		case constraints.HasLt():
			isOr = constraints.GetLt() <= constraints.GetGte()
		case constraints.HasLte():
			isOr = constraints.GetLte() < constraints.GetGte()
		}
		if isOr {
			orNumberSchema = map[string]interface{}{"minimum": constraints.GetGte()}
		} else {
			numberSchema["minimum"] = constraints.GetGte()
		}
	default:
		numberSchema["minimum"] = 0
	}
	switch {
	case constraints.HasLt():
		numberSchema["exclusiveMaximum"] = constraints.GetLt()
	case constraints.HasLte():
		numberSchema["maximum"] = constraints.GetLte()
	default:
		numberSchema["exclusiveMaximum"] = maxExclVal
	}

	anyOf := []map[string]interface{}{
		numberSchema,
	}
	if bits > 52 {
		anyOf = append(anyOf, map[string]interface{}{
			"type":    jsString,
			"pattern": "^[0-9]+$",
		})
	}
	if orNumberSchema != nil {
		numberSchema["minimum"] = 0
		orNumberSchema["exclusiveMaximum"] = maxExclVal
		orNumberSchema["type"] = jsInteger
		anyOf = append(anyOf, orNumberSchema)
	}
	if len(anyOf) > 1 {
		schema["anyOf"] = anyOf
	} else {
		for key, value := range numberSchema {
			schema[key] = value
		}
	}
}
func (p *jsonSchemaGenerator) generateUint32Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	switch {
	default:
		schema["type"] = jsInteger
		schema["minimum"] = 0
		schema["maximum"] = math.MaxUint32
	case constraints.GetUint32() != nil:
		generateUintValidation(constraints.GetUint32(), 32, schema)
	case constraints.GetFixed32() != nil:
		generateUintValidation(constraints.GetFixed32(), 32, schema)
	}
}

func (p *jsonSchemaGenerator) generateUint64Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	switch {
	default:
		schema["anyOf"] = []map[string]interface{}{
			{"type": jsInteger, "minimum": 0, "exclusiveMaximum": float64(uint64(1)<<63) * 2},
			{"type": jsString, "pattern": "^[0-9]+$"},
		}
	case constraints.GetUint64() != nil:
		generateUintValidation(constraints.GetUint64(), 64, schema)
	case constraints.GetFixed64() != nil:
		generateUintValidation(constraints.GetFixed64(), 64, schema)
	}
}

// nolint: gocyclo
func (p *jsonSchemaGenerator) generateFloatValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}, bits int) {
	includePosInf := true
	includeNegInf := true
	includeNaN := true

	numberSchema := map[string]interface{}{
		"type": jsNumber,
	}
	var orNumberSchema map[string]interface{}

	switch {
	default:
		if bits == 32 {
			numberSchema["minimum"] = -math.MaxFloat32
			numberSchema["maximum"] = math.MaxFloat32
		}
	case constraints.GetFloat() != nil:
		if constraints.GetFloat().GetFinite() {
			includePosInf = false
			includeNegInf = false
			includeNaN = false
		}
		if constraints.GetFloat().Const != nil {
			numberSchema["enum"] = []float32{constraints.GetFloat().GetConst()}
			includePosInf = false
			includeNegInf = false
			includeNaN = false
			if math.IsInf(float64(constraints.GetFloat().GetConst()), 1) {
				includePosInf = true
			}
			if math.IsInf(float64(constraints.GetFloat().GetConst()), -1) {
				includeNegInf = true
			}
			if math.IsNaN(float64(constraints.GetFloat().GetConst())) {
				includeNaN = true
			}
		}
		if len(constraints.GetFloat().GetIn()) > 0 {
			numberSchema["enum"] = constraints.GetFloat().GetIn()
			includePosInf = false
			includeNegInf = false
			includeNaN = false
			for _, value := range constraints.GetFloat().GetIn() {
				if math.IsInf(float64(value), 1) {
					includePosInf = true
				}
				if math.IsInf(float64(value), -1) {
					includeNegInf = true
				}
				if math.IsNaN(float64(value)) {
					includeNaN = true
				}
			}
		}
		switch greaterThan := constraints.GetFloat().GetGreaterThan().(type) {
		case *validate.FloatRules_Gt:
			includeNaN = false
			var isOr bool
			switch lessThan := constraints.GetFloat().GetLessThan().(type) {
			case *validate.FloatRules_Lt:
				isOr = lessThan.Lt <= greaterThan.Gt
			case *validate.FloatRules_Lte:
				isOr = lessThan.Lte <= greaterThan.Gt
			}
			if isOr {
				orNumberSchema = map[string]interface{}{
					"type":             jsNumber,
					"exclusiveMinimum": greaterThan.Gt,
				}
			} else {
				includeNegInf = false
				numberSchema["exclusiveMinimum"] = greaterThan.Gt
			}
		case *validate.FloatRules_Gte:
			includeNaN = false
			isOr := false
			switch lessThan := constraints.GetFloat().GetLessThan().(type) {
			case *validate.FloatRules_Lt:
				isOr = lessThan.Lt <= greaterThan.Gte
			case *validate.FloatRules_Lte:
				isOr = lessThan.Lte < greaterThan.Gte
			}
			if isOr {
				orNumberSchema = map[string]interface{}{
					"type":    jsNumber,
					"minimum": greaterThan.Gte,
				}
			} else {
				if greaterThan.Gte != float32(math.Inf(-1)) {
					includeNegInf = false
				}
				numberSchema["minimum"] = greaterThan.Gte
			}
		default:
			numberSchema["minimum"] = -math.MaxFloat32
		}
		switch lessThan := constraints.GetFloat().GetLessThan().(type) {
		case *validate.FloatRules_Lt:
			includeNaN = false
			if orNumberSchema == nil {
				includePosInf = false
			}
			numberSchema["exclusiveMaximum"] = lessThan.Lt
		case *validate.FloatRules_Lte:
			includeNaN = false
			if lessThan.Lte != float32(math.Inf(1)) && orNumberSchema == nil {
				includePosInf = false
			}
			numberSchema["maximum"] = lessThan.Lte
		default:
			numberSchema["maximum"] = math.MaxFloat32
		}
	case constraints.GetDouble() != nil:
		if constraints.GetDouble().GetFinite() {
			includePosInf = false
			includeNegInf = false
			includeNaN = false
		}
		if constraints.GetDouble().Const != nil {
			numberSchema["enum"] = []float64{constraints.GetDouble().GetConst()}
			includePosInf = false
			includeNegInf = false
			includeNaN = false
			if math.IsInf(constraints.GetDouble().GetConst(), 1) {
				includePosInf = true
			}
			if math.IsInf(constraints.GetDouble().GetConst(), -1) {
				includeNegInf = true
			}
			if math.IsNaN(constraints.GetDouble().GetConst()) {
				includeNaN = true
			}
		}
		if len(constraints.GetDouble().GetIn()) > 0 {
			numberSchema["enum"] = constraints.GetDouble().GetIn()
			includePosInf = false
			includeNegInf = false
			includeNaN = false
			for _, value := range constraints.GetDouble().GetIn() {
				if math.IsInf(value, 1) {
					includePosInf = true
				}
				if math.IsInf(value, -1) {
					includeNegInf = true
				}
				if math.IsNaN(value) {
					includeNaN = true
				}
			}
		}
		switch greaterThan := constraints.GetDouble().GetGreaterThan().(type) {
		case *validate.DoubleRules_Gt:
			includeNaN = false
			var isOr bool
			switch lessThan := constraints.GetDouble().GetLessThan().(type) {
			case *validate.DoubleRules_Lt:
				isOr = lessThan.Lt <= greaterThan.Gt
			case *validate.DoubleRules_Lte:
				isOr = lessThan.Lte <= greaterThan.Gt
			}
			if isOr {
				orNumberSchema = map[string]interface{}{
					"type":             jsNumber,
					"exclusiveMinimum": greaterThan.Gt,
				}
			} else {
				includeNegInf = false
				numberSchema["exclusiveMinimum"] = greaterThan.Gt
			}
		case *validate.DoubleRules_Gte:
			includeNaN = false
			isOr := false
			switch lessThan := constraints.GetDouble().GetLessThan().(type) {
			case *validate.DoubleRules_Lt:
				isOr = lessThan.Lt <= greaterThan.Gte
			case *validate.DoubleRules_Lte:
				isOr = lessThan.Lte < greaterThan.Gte
			}
			if isOr {
				orNumberSchema = map[string]interface{}{
					"type":    jsNumber,
					"minimum": greaterThan.Gte,
				}
			} else {
				if greaterThan.Gte != math.Inf(-1) {
					includeNegInf = false
				}
				numberSchema["minimum"] = greaterThan.Gte
			}
		}
		switch lessThan := constraints.GetDouble().GetLessThan().(type) {
		case *validate.DoubleRules_Lt:
			includeNaN = false
			if orNumberSchema == nil {
				includePosInf = false
			}
			numberSchema["exclusiveMaximum"] = lessThan.Lt
		case *validate.DoubleRules_Lte:
			includeNaN = false
			if lessThan.Lte != math.Inf(1) && orNumberSchema == nil {
				includePosInf = false
			}
			numberSchema["maximum"] = lessThan.Lte
		}
	}

	anyOf := []map[string]interface{}{
		numberSchema,
	}
	if orNumberSchema != nil {
		anyOf = append(anyOf, orNumberSchema)
	}

	extremaEnum := []interface{}{}
	if includePosInf {
		extremaEnum = append(extremaEnum, "Infinity")
	}
	if includeNegInf {
		extremaEnum = append(extremaEnum, "-Infinity")
	}
	if includeNaN {
		extremaEnum = append(extremaEnum, "NaN")
	}
	if len(extremaEnum) > 0 {
		anyOf = append(anyOf, map[string]interface{}{
			"type": jsString,
			"enum": extremaEnum,
		}, map[string]interface{}{
			"type": jsString, // Allow other form of NaN, -Infinity, and Infinity.
		})
	} else {
		anyOf = append(anyOf, map[string]interface{}{
			"type":    jsString,
			"pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$",
		})
	}

	schema["anyOf"] = anyOf
}

const (
	ipv4PatternBit     = "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
	ipv6PatternBit     = "(([0-9a-fA-F]{1,4}::?){1,7}([0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,7}:|:((([0-9a-fA-F]{1,4}:){1,6})?[0-9a-fA-F]{1,4})?|::)"
	ipv4LenPatternBit  = "/([0-9]|[12][0-9]|3[0-2])"
	ipv6LenPatternBit  = "/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])"
	portPatternBit     = "([1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"
	hostnamePatternBit = "[A-Za-z0-9][A-Za-z0-9-]{0,63}(\\.[A-Za-z0-9-][A-Za-z0-9-]{0,63})*"

	ipv4Pattern          = "^" + ipv4PatternBit + "$"
	ipv6Pattern          = "^" + ipv6PatternBit + "$"
	hostnamePattern      = "^" + hostnamePatternBit + "$"
	uriPattern           = "^(?:(?:[a-zA-Z][a-zA-Z\\d+\\-.]*):)?(?://(?:[A-Za-z0-9\\-\\.]+(?::\\d+)?))?(/[^\\?#]*)?(?:\\?([^\\#]*))?(?:\\#(.*))?$"
	uriRefPattern        = "^(?:(?:[a-zA-Z][a-zA-Z\\d+\\-.]*):)?(?:\\/\\/(?:[A-Za-z0-9\\-\\.]+(?::\\d+)?))?(/[^\\?#]*)?(?:\\?([^\\#]*))?(?:\\#(.*))?$"
	uuidPattern          = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
	tuuidPattern         = "^[0-9a-fA-F]{32}$"
	ipv4PrefixLenPattern = "^" + ipv4PatternBit + ipv4LenPatternBit + "$"
	ipv6PrefixLenPattern = "^" + ipv6PatternBit + ipv6LenPatternBit + "$"
	ipv4PrefixPattern    = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}0/([0-9]|[12][0-9]|3[0-2])$"
	ipv6PrefixPattern    = "^(([0-9a-fA-F]{1,4}:){1,7}:|::)/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$"
	hostAndPortPattern   = "^(" + hostnamePatternBit + "|" + ipv4PatternBit + "|\\[" + ipv6PatternBit + "\\]):" + portPatternBit + "$"
)

// nolint: gocyclo
func generateWellKnownPattern(constraints *validate.FieldConstraints, schema map[string]interface{}) {
	switch wellKnown := constraints.GetString().GetWellKnown().(type) {
	case *validate.StringRules_Hostname:
		if wellKnown.Hostname {
			schema["pattern"] = hostnamePattern
		}
	case *validate.StringRules_Email:
		if wellKnown.Email {
			schema["format"] = "email"
		}
	case *validate.StringRules_Ip:
		if wellKnown.Ip {
			schema["pattern"] = fmt.Sprintf("%s|%s", ipv4Pattern, ipv6Pattern)
		}
	case *validate.StringRules_Ipv4:
		if wellKnown.Ipv4 {
			schema["format"] = "ipv4"
		}
	case *validate.StringRules_Ipv6:
		if wellKnown.Ipv6 {
			schema["format"] = "ipv6"
		}
	case *validate.StringRules_Uri:
		if wellKnown.Uri {
			schema["pattern"] = uriPattern
		}
	case *validate.StringRules_UriRef:
		if wellKnown.UriRef {
			schema["pattern"] = uriRefPattern
		}
	case *validate.StringRules_Address:
		if wellKnown.Address {
			schema["pattern"] = fmt.Sprintf("%s|%s|%s", ipv4Pattern, ipv6Pattern, hostnamePattern)
		}
	case *validate.StringRules_Uuid:
		if wellKnown.Uuid {
			schema["pattern"] = uuidPattern
		}
	case *validate.StringRules_Tuuid:
		if wellKnown.Tuuid {
			schema["pattern"] = tuuidPattern
		}
	case *validate.StringRules_Ipv4WithPrefixlen:
		if wellKnown.Ipv4WithPrefixlen {
			schema["pattern"] = ipv4PrefixLenPattern
		}
	case *validate.StringRules_Ipv6WithPrefixlen:
		if wellKnown.Ipv6WithPrefixlen {
			schema["pattern"] = ipv6PrefixLenPattern
		}
	case *validate.StringRules_IpWithPrefixlen:
		if wellKnown.IpWithPrefixlen {
			schema["pattern"] = fmt.Sprintf("%s|%s", ipv4PrefixLenPattern, ipv6PrefixLenPattern)
		}
	case *validate.StringRules_Ipv4Prefix:
		if wellKnown.Ipv4Prefix {
			schema["pattern"] = ipv4PrefixPattern
		}
	case *validate.StringRules_Ipv6Prefix:
		if wellKnown.Ipv6Prefix {
			schema["pattern"] = ipv6PrefixPattern
		}
	case *validate.StringRules_IpPrefix:
		if wellKnown.IpPrefix {
			schema["pattern"] = fmt.Sprintf("%s|%s", ipv4PrefixPattern, ipv6PrefixPattern)
		}
	case *validate.StringRules_HostAndPort:
		if wellKnown.HostAndPort {
			schema["pattern"] = hostAndPortPattern
		}
	case *validate.StringRules_WellKnownRegex:
		if wellKnown.WellKnownRegex == validate.KnownRegex_KNOWN_REGEX_HTTP_HEADER_NAME &&
			constraints.GetString().GetStrict() {
			schema["pattern"] = "^:?[0-9a-zA-Z!#$%&\\'*+-.^_|~\\x60]+$"
		}
	}
}

func (p *jsonSchemaGenerator) generateStringValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	schema["type"] = jsString
	if constraints.GetString() == nil {
		return
	}

	// Bytes are <= Characters, so we can only enforce an upper bound.
	if constraints.GetString().LenBytes != nil {
		schema["maxLength"] = constraints.GetString().GetMaxBytes()
	} else if constraints.GetString().MaxBytes != nil {
		schema["maxLength"] = constraints.GetString().GetMaxBytes()
	}

	if constraints.GetString().Len != nil {
		schema["minLength"] = constraints.GetString().GetLen()
		schema["maxLength"] = constraints.GetString().GetLen()
	} else {
		if constraints.GetString().MinLen != nil && constraints.GetString().GetMinLen() > 0 {
			schema["minLength"] = constraints.GetString().GetMinLen()
		} else if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE {
			schema["minLength"] = 1
		}
		if constraints.GetString().MaxLen != nil {
			schema["maxLength"] = constraints.GetString().GetMaxLen()
		}
	}

	generateWellKnownPattern(constraints, schema)

	switch {
	case constraints.GetString().Pattern != nil:
		schema["pattern"] = constraints.GetString().GetPattern()
	case constraints.GetString().Prefix != nil,
		constraints.GetString().Suffix != nil,
		constraints.GetString().Contains != nil:
		pattern := ""
		if constraints.GetString().Prefix != nil {
			pattern += "^" + constraints.GetString().GetPrefix()
		}
		pattern += ".*"
		if constraints.GetString().Contains != nil {
			pattern += constraints.GetString().GetContains()
			pattern += ".*"
		}
		if constraints.GetString().Suffix != nil {
			pattern += constraints.GetString().GetSuffix() + "$"
		}
		schema["pattern"] = pattern
	}

	if constraints.GetString().Const != nil {
		schema["enum"] = []string{constraints.GetString().GetConst()}
	} else if len(constraints.GetString().In) > 0 {
		schema["enum"] = constraints.GetString().GetIn()
	}
}

func base64EncodedLength(inputSize uint64) (uint64, uint64) {
	// Base64 encoding is 4/3 the size of the input.
	// Padding is added to make the output size a multiple of 4.
	// For example 5 bytes is encoded as
	characters := inputSize * 4 / 3
	if inputSize%3 != 0 {
		characters++
	}
	padding := 4 - (characters % 4)
	return characters, padding
}

func (p *jsonSchemaGenerator) generateBytesValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) {
	schema["type"] = jsString
	// Set a regex to match base64 encoded strings.
	schema["pattern"] = "^[A-Za-z0-9+/]*={0,2}$"
	if constraints.GetBytes() == nil {
		return
	}

	if constraints.GetBytes().Len != nil {
		size, padding := base64EncodedLength(constraints.GetBytes().GetLen())
		schema["minLength"] = size
		schema["maxLength"] = size + padding
	} else {
		if constraints.GetBytes().MaxLen != nil {
			size, padding := base64EncodedLength(constraints.GetBytes().GetMaxLen())
			schema["maxLength"] = size + padding
		}
		if constraints.GetBytes().MinLen != nil {
			size, _ := base64EncodedLength(constraints.GetBytes().GetMinLen())
			schema["minLength"] = size
		} else if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE {
			schema["minLength"] = 1
		}
	}
}

func (p *jsonSchemaGenerator) generateMessageValidation(field protoreflect.FieldDescriptor, schema map[string]interface{}) {
	p.generate(field.Message(), schema)
}

func (p *jsonSchemaGenerator) generateWrapperValidation(
	desc protoreflect.MessageDescriptor,
	constraints *validate.FieldConstraints,
	schema map[string]interface{},
) {
	field := desc.Fields().Get(0)
	p.setDescription(field, schema)
	p.generateValidation(field, constraints, schema)
}

func (p *jsonSchemaGenerator) makeWktGenerators() map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{}) {
	var result = make(map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{}))
	result["google.protobuf.Any"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsObject
		schema["properties"] = map[string]interface{}{
			"@type": map[string]interface{}{
				"type": "string",
			},
		}
	}

	result["google.protobuf.Duration"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsString
		schema["format"] = "duration"
	}
	result["google.protobuf.Timestamp"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsString
		schema["format"] = "date-time"
	}

	result["google.protobuf.Value"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, _ map[string]interface{}) {}
	result["google.protobuf.ListValue"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsArray
	}
	result["google.protobuf.NullValue"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsNull
	}
	result["google.protobuf.Struct"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) {
		schema["type"] = jsObject
	}

	result["google.protobuf.BoolValue"] = p.generateWrapperValidation
	result["google.protobuf.BytesValue"] = p.generateWrapperValidation
	result["google.protobuf.DoubleValue"] = p.generateWrapperValidation
	result["google.protobuf.FloatValue"] = p.generateWrapperValidation
	result["google.protobuf.Int32Value"] = p.generateWrapperValidation
	result["google.protobuf.Int64Value"] = p.generateWrapperValidation
	result["google.protobuf.StringValue"] = p.generateWrapperValidation
	result["google.protobuf.UInt32Value"] = p.generateWrapperValidation
	result["google.protobuf.UInt64Value"] = p.generateWrapperValidation
	return result
}

func (p *jsonSchemaGenerator) shouldIgnoreField(fdesc protoreflect.FieldDescriptor) FieldVisibility {
	const ignoreComment = "jsonschema:ignore"
	const hideComment = "jsonschema:hide"
	srcLoc := fdesc.ParentFile().SourceLocations().ByDescriptor(fdesc)
	switch {
	case strings.Contains(srcLoc.LeadingComments, ignoreComment),
		strings.Contains(srcLoc.TrailingComments, ignoreComment):
		return FieldIgnore
	case strings.Contains(srcLoc.LeadingComments, hideComment),
		strings.Contains(srcLoc.TrailingComments, hideComment):
		return FieldHide
	default:
		return FieldVisible
	}
}
