// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

// This file contains functions that infer a schema from a Go type.

package jsonschema

import (
	
	
	
	
	
	
	
	
	
	
)

const debugEnv = "JSONSCHEMAGODEBUG"

// ForOptions are options for the [For] and [ForType] functions.
type ForOptions struct {
	// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
	// Schema are ignored instead of causing an error.
	// This allows callers to adjust the resulting schema using custom knowledge.
	// For example, an interface type where all the possible implementations are
	// known can be described with "oneof".
	IgnoreInvalidTypes bool

	// TypeSchemas maps types to their schemas.
	// If [For] encounters a type that is a key in this map, the
	// corresponding value is used as the resulting schema (after cloning to
	// ensure uniqueness).
	// Types in this map override the default translations, as described
	// in [For]'s documentation.
	// PropertyOrder defined in these schemas will not be used in [For] or [ForType].
	TypeSchemas map[reflect.Type]*Schema
}

// For constructs a JSON schema object for the given type argument.
// If non-nil, the provided options configure certain aspects of this contruction,
// described below.

// It translates Go types into compatible JSON schema types, as follows.
// These defaults can be overridden by [ForOptions.TypeSchemas].
//
//   - Strings have schema type "string".
//   - Bools have schema type "boolean".
//   - Signed and unsigned integer types have schema type "integer".
//   - Floating point types have schema type "number".
//   - Slices and arrays have schema type "array", and a corresponding schema
//     for items.
//   - Maps with string key have schema type "object", and corresponding
//     schema for additionalProperties.
//   - Structs have schema type "object", and disallow additionalProperties.
//     Their properties are derived from exported struct fields, using the
//     struct field JSON name. Fields that are marked "omitempty" or "omitzero" are
//     considered optional; all other fields become required properties.
//     For structs, the PropertyOrder will be set to the field order.
//   - Some types in the standard library that implement json.Marshaler
//     translate to schemas that match the values to which they marshal.
//     For example, [time.Time] translates to the schema for strings.
//
// For will return an error if there is a cycle in the types.
//
// By default, For returns an error if t contains (possibly recursively) any of the
// following Go types, as they are incompatible with the JSON schema spec.
// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.
//   - maps with key other than 'string'
//   - function types
//   - channel types
//   - complex numbers
//   - unsafe pointers
//
// This function recognizes struct field tags named "jsonschema".
// A jsonschema tag on a field is used as the description for the corresponding property.
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
// sequence of non-whitespace characters.
func [ any]( *ForOptions) (*Schema, error) {
	if  == nil {
		 = &ForOptions{}
	}
	 := maps.Clone(initialSchemaMap)
	// Add types from the options. They override the default ones.
	maps.Copy(, .TypeSchemas)
	,  := forType(reflect.TypeFor[](), map[reflect.Type]bool{}, .IgnoreInvalidTypes, )
	if  != nil {
		var  
		return nil, fmt.Errorf("For[%T](): %w", , )
	}
	return , nil
}

// ForType is like [For], but takes a [reflect.Type]
func ( reflect.Type,  *ForOptions) (*Schema, error) {
	if  == nil {
		 = &ForOptions{}
	}
	 := maps.Clone(initialSchemaMap)
	// Add types from the options. They override the default ones.
	maps.Copy(, .TypeSchemas)
	,  := forType(, map[reflect.Type]bool{}, .IgnoreInvalidTypes, )
	if  != nil {
		return nil, fmt.Errorf("ForType(%s): %w", , )
	}
	return , nil
}

// Helper to create a *float64 pointer from a value
func f64Ptr( float64) *float64 {
	return &
}

func forType( reflect.Type,  map[reflect.Type]bool,  bool,  map[reflect.Type]*Schema) (*Schema, error) {
	// Follow pointers: the schema for *T is almost the same as for T, except that
	// an explicit JSON "null" is allowed for the pointer.
	 := false
	for .Kind() == reflect.Pointer {
		 = true
		 = .Elem()
	}

	// Check for cycles
	// User defined types have a name, so we can skip those that are natively defined
	if .Name() != "" {
		if [] {
			return nil, fmt.Errorf("cycle detected for type %v", )
		}
		[] = true
		defer delete(, )
	}

	if  := [];  != nil {
		 := .CloneSchemas()
		if os.Getenv(debugEnv) != "typeschemasnull=1" &&  {
			if .Type != "" {
				.Types = []string{"null", .Type}
				.Type = ""
			} else if !slices.Contains(.Types, "null") {
				.Types = append([]string{"null"}, .Types...)
			}
		}
		return , nil
	}

	var (
		   = new(Schema)
		 error
	)

	switch .Kind() {
	case reflect.Bool:
		.Type = "boolean"

	case reflect.Int, reflect.Int64:
		.Type = "integer"

	case reflect.Uint, reflect.Uint64, reflect.Uintptr:
		.Type = "integer"
		.Minimum = f64Ptr(0)

	case reflect.Int8:
		.Type = "integer"
		.Minimum = f64Ptr(math.MinInt8)
		.Maximum = f64Ptr(math.MaxInt8)

	case reflect.Uint8:
		.Type = "integer"
		.Minimum = f64Ptr(0)
		.Maximum = f64Ptr(math.MaxUint8)

	case reflect.Int16:
		.Type = "integer"
		.Minimum = f64Ptr(math.MinInt16)
		.Maximum = f64Ptr(math.MaxInt16)

	case reflect.Uint16:
		.Type = "integer"
		.Minimum = f64Ptr(0)
		.Maximum = f64Ptr(math.MaxUint16)

	case reflect.Int32:
		.Type = "integer"
		.Minimum = f64Ptr(math.MinInt32)
		.Maximum = f64Ptr(math.MaxInt32)

	case reflect.Uint32:
		.Type = "integer"
		.Minimum = f64Ptr(0)
		.Maximum = f64Ptr(math.MaxUint32)

	case reflect.Float32, reflect.Float64:
		.Type = "number"

	case reflect.Interface:
		// Unrestricted

	case reflect.Map:
		if .Key().Kind() != reflect.String {
			if  {
				return nil, nil // ignore
			}
			return nil, fmt.Errorf("unsupported map key type %v", .Key().Kind())
		}
		if .Key().Kind() != reflect.String {
		}
		.Type = "object"
		.AdditionalProperties,  = (.Elem(), , , )
		if  != nil {
			return nil, fmt.Errorf("computing map value schema: %v", )
		}
		if  && .AdditionalProperties == nil {
			// Ignore if the element type is invalid.
			return nil, nil
		}

	case reflect.Slice, reflect.Array:
		if os.Getenv(debugEnv) != "typeschemasnull=1" && .Kind() == reflect.Slice {
			.Types = []string{"null", "array"}
		} else {
			.Type = "array"
		}
		,  := (.Elem(), , , )
		if  != nil {
			return nil, fmt.Errorf("computing element schema: %v", )
		}
		if  == nil {
			return nil, nil
		}
		.Items = 
		if  && .Items == nil {
			// Ignore if the element type is invalid.
			return nil, nil
		}
		if .Kind() == reflect.Array {
			.MinItems = Ptr(.Len())
			.MaxItems = Ptr(.Len())
		}

	case reflect.String:
		.Type = "string"

	case reflect.Struct:
		.Type = "object"
		// no additional properties are allowed
		.AdditionalProperties = falseSchema()

		// If skipPath is non-nil, it is path to an anonymous field whose
		// schema has been replaced by a known schema.
		var  []int
		for ,  := range reflect.VisibleFields() {
			if .Properties == nil {
				.Properties = make(map[string]*Schema)
			}
			if .Anonymous {
				 := [.Type]
				if  != nil {
					// Type must be object, and only properties can be set.
					if .Type != "object" {
						return nil, fmt.Errorf(`custom schema for embedded struct must have type "object", got %q`,
							.Type)
					}
					// Check that all keywords relevant for objects are absent, except properties.
					 := reflect.ValueOf().Elem()
					for ,  := range schemaFieldInfos {
						if .sf.Name == "Type" || .sf.Name == "Properties" {
							continue
						}
						 := .FieldByIndex(.sf.Index)
						if !.IsZero() {
							return nil, fmt.Errorf(`overrides for embedded fields can have only "Type" and "Properties"; this has %q`, .sf.Name)
						}
					}

					 = .Index
					 := make([]string, 0, len(.Properties))
					for  := range .Properties {
						 = append(, )
					}
					slices.Sort()
					for ,  := range  {
						if ,  := .Properties[]; ! {
							.Properties[] = .Properties[].CloneSchemas()
							.PropertyOrder = append(.PropertyOrder, )
						}
					}
				}
				continue
			}

			// Check to see if this field has been promoted from a replaced anonymous
			// type.
			if  != nil {
				 := false
				if len(.Index) >= len() {
					 = true
					for ,  := range  {
						if .Index[] !=  {
							// If we're no longer in a subfield.
							 = false
							break
						}
					}
				}
				if  {
					continue
				} else {
					// Anonymous fields are followed immediately by their promoted fields.
					// Once we encounter a field that *isn't* promoted, we can stop
					// checking.
					 = nil
				}
			}

			 := fieldJSONInfo()
			if .omit {
				continue
			}
			,  := (.Type, , , )
			if  != nil {
				return nil, 
			}
			if  &&  == nil {
				// Skip fields of invalid type.
				continue
			}
			if ,  := .Tag.Lookup("jsonschema");  {
				if  == "" {
					return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", , .Name)
				}
				if disallowedPrefixRegexp.MatchString() {
					return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", )
				}
				.Description = 
			}
			.Properties[.name] = 

			.PropertyOrder = append(.PropertyOrder, .name)

			if !.settings["omitempty"] && !.settings["omitzero"] {
				.Required = append(.Required, .name)
			}
		}

		// Remove PropertyOrder duplicates, keeping the last occurrence
		if len(.PropertyOrder) > 1 {
			 := make(map[string]bool)
			// Create a slice to hold the cleaned order (capacity = current length)
			 := make([]string, 0, len(.PropertyOrder))

			// Iterate backwards
			for  := len(.PropertyOrder) - 1;  >= 0; -- {
				 := .PropertyOrder[]
				if ![] {
					 = append(, )
					[] = true
				}
			}

			// Since we collected them backwards, we need to reverse the result
			// to restore the correct order.
			slices.Reverse()
			.PropertyOrder = 
		}

	default:
		if  {
			// Ignore.
			return nil, nil
		}
		return nil, fmt.Errorf("type %v is unsupported by jsonschema", )
	}
	if  && .Type != "" {
		.Types = []string{"null", .Type}
		.Type = ""
	}
	return , nil
}

// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
var initialSchemaMap = make(map[reflect.Type]*Schema)

func init() {
	 := &Schema{Type: "string"}
	initialSchemaMap[reflect.TypeFor[time.Time]()] = 
	initialSchemaMap[reflect.TypeFor[slog.Level]()] = 
	if os.Getenv(debugEnv) == "typeschemasnull=1" {
		initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
	} else {
		initialSchemaMap[reflect.TypeFor[big.Int]()] = 
	}
	initialSchemaMap[reflect.TypeFor[big.Rat]()] = 
	initialSchemaMap[reflect.TypeFor[big.Float]()] = 
}

// Disallow jsonschema tag values beginning "WORD=", for future expansion.
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")