// Generator generates Go code from a parsed schema.
package schemagen

import (
	"bytes"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"unicode"
)

// NameOverrides maps schema names to preferred Go names (like Python SDK's RENAME_MAP).
// Maps from the generated/auto-inferred name to the preferred name.
var NameOverrides = map[string]string{
	// ContentBlock variants
	"ContentBlock1": "TextContent",
	"ContentBlock2": "ImageContent",
	"ContentBlock3": "AudioContent",
	"ContentBlock4": "ResourceLink",
	"ContentBlock5": "EmbeddedResource",
	// McpServer variants
	"McpServer1": "HttpMcpServer",
	"McpServer2": "SseMcpServer",
	// AuthMethod variants
	"AuthMethod1": "EnvVarAuthMethod",
	"AuthMethod2": "TerminalAuthMethod",
	// ToolCallContent variants
	"ToolCallContent1": "ContentToolCallContent",
	"ToolCallContent2": "FileEditToolCallContent",
	"ToolCallContent3": "TerminalToolCallContent",
	// SessionUpdate variants
	"SessionUpdate1":  "UserMessageChunk",
	"SessionUpdate2":  "AgentMessageChunk",
	"SessionUpdate3":  "AgentThoughtChunk",
	"SessionUpdate4":  "ToolCallStart",
	"SessionUpdate5":  "ToolCallProgress",
	"SessionUpdate6":  "AgentPlanUpdate",
	"SessionUpdate7":  "AvailableCommandsUpdate",
	"SessionUpdate8":  "CurrentModeUpdate",
	"SessionUpdate9":  "ConfigOptionUpdate",
	"SessionUpdate10": "SessionInfoUpdate",
	"SessionUpdate11": "UsageUpdate",
	// PermissionOutcome
	"RequestPermissionOutcome1": "DeniedOutcome",
	"RequestPermissionOutcome2": "AllowedOutcome",
}

// FileAssignment maps schema type names to output Go files.
var FileAssignment = map[string]string{
	"Role":                           "types.go",
	"PermissionOptionKind":           "types.go",
	"PlanEntryPriority":              "types.go",
	"PlanEntryStatus":                "types.go",
	"StopReason":                     "types.go",
	"ToolCallStatus":                 "types.go",
	"ToolKind":                       "types.go",
	"ErrorCode":                      "types.go",
	"SessionId":                      "types.go",
	"ToolCallId":                     "types.go",
	"TerminalId":                     "types.go",
	"RequestId":                      "types.go",
	"SessionConfigId":                "types.go",
	"SessionConfigOptionCategory":    "types.go",
	"SessionConfigValueId":           "types.go",
	"SessionModeId":                  "types.go",
	"Meta":                           "types.go",
	"Implementation":                 "types.go",
	"EnvVariable":                    "types.go",
	"HttpHeader":                     "types.go",
	"McpServerStdio":                 "types.go",
	"McpServerHttp":                  "types.go",
	"McpServerSse":                   "types.go",
	"AuthMethodAgent":                "types.go",
	"AuthMethodEnvVar":               "types.go",
	"AuthMethodTerminal":             "types.go",
	"AuthEnvVar":                     "types.go",
	"AuthCapabilities":               "types.go",
	"ModelInfo":                      "types.go",
	"Diff":                           "types.go",
	"Cost":                           "types.go",
	"Usage":                          "types.go",
	"SessionInfo":                    "types.go",
	"Terminal":                       "types.go",
	"TerminalExitStatus":             "types.go",
	"ToolCallLocation":               "types.go",
	"Annotations":                    "content.go",
	"TextContent":                    "content.go",
	"ImageContent":                   "content.go",
	"AudioContent":                   "content.go",
	"ResourceLink":                   "content.go",
	"EmbeddedResource":               "content.go",
	"EmbeddedResourceResource":       "content.go",
	"TextResourceContents":           "content.go",
	"BlobResourceContents":           "content.go",
	"ContentBlock":                   "content.go",
	"ToolCall":                       "tool_call.go",
	"ToolCallUpdate":                 "tool_call.go",
	"ToolCallUpdateFields":           "tool_call.go",
	"ToolCallContent":                "tool_call.go",
	"ToolContent":                    "tool_call.go",
	"TerminalRef":                    "tool_call.go",
	"PromptCapabilities":             "capabilities.go",
	"McpCapabilities":                "capabilities.go",
	"FileSystemCapabilities":         "capabilities.go",
	"SessionCapabilities":            "capabilities.go",
	"SessionCloseCapabilities":       "capabilities.go",
	"SessionForkCapabilities":        "capabilities.go",
	"SessionListCapabilities":        "capabilities.go",
	"SessionResumeCapabilities":      "capabilities.go",
	"AgentCapabilities":              "capabilities.go",
	"ClientCapabilities":             "capabilities.go",
	"SessionMode":                    "session.go",
	"SessionModeState":               "session.go",
	"SessionModelState":              "session.go",
	"SessionConfigSelectOption":      "session.go",
	"SessionConfigSelectGroup":       "session.go",
	"ConfigOption":                   "session.go",
	"SessionConfigOptionBoolean":     "session.go",
	"SessionConfigOptionSelect":      "session.go",
	"ConfigSelectOptions":            "session.go",
	"SessionUpdate":                  "session.go",
	"ContentChunk":                   "session.go",
	"Plan":                           "session.go",
	"PlanEntry":                      "session.go",
	"AvailableCommand":               "session.go",
	"AvailableCommandInput":          "session.go",
	"AvailableCommandsUpdate":        "session.go",
	"CurrentModeUpdate":              "session.go",
	"ConfigOptionUpdate":             "session.go",
	"SessionInfoUpdate":              "session.go",
	"InitializeRequest":              "requests.go",
	"InitializeResponse":             "requests.go",
	"AuthenticateRequest":            "requests.go",
	"AuthenticateResponse":           "requests.go",
	"NewSessionRequest":              "requests.go",
	"NewSessionResponse":             "requests.go",
	"LoadSessionRequest":             "requests.go",
	"LoadSessionResponse":            "requests.go",
	"ListSessionsRequest":            "requests.go",
	"ListSessionsResponse":           "requests.go",
	"PromptRequest":                  "requests.go",
	"PromptResponse":                 "requests.go",
	"SetSessionModeRequest":          "requests.go",
	"SetSessionModeResponse":         "requests.go",
	"SetSessionConfigOptionRequest":  "requests.go",
	"SetSessionConfigOptionResponse": "requests.go",
	"PermissionOption":               "requests.go",
	"RequestPermissionRequest":       "requests.go",
	"RequestPermissionResponse":      "requests.go",
	"PermissionOutcome":              "requests.go",
	"ReadTextFileRequest":            "requests.go",
	"ReadTextFileResponse":           "requests.go",
	"WriteTextFileRequest":           "requests.go",
	"WriteTextFileResponse":          "requests.go",
	"CreateTerminalRequest":          "requests.go",
	"CreateTerminalResponse":         "requests.go",
	"TerminalOutputRequest":          "requests.go",
	"TerminalOutputResponse":         "requests.go",
	"ReleaseTerminalRequest":         "requests.go",
	"ReleaseTerminalResponse":        "requests.go",
	"WaitForTerminalExitRequest":     "requests.go",
	"WaitForTerminalExitResponse":    "requests.go",
	"KillTerminalRequest":            "requests.go",
	"KillTerminalResponse":           "requests.go",
	"Error":                          "requests.go",
	"CancelNotification":             "notifications.go",
	"SessionNotification":            "notifications.go",
}

// DefaultFile is the default output file for unassigned types.
const DefaultFile = "types.go"

// Generator generates Go code.
type Generator struct {
	Schema *Schema

	// Each definition gets its own buffer to avoid nesting issues
	defBufs map[string]*bytes.Buffer
}

// NewGenerator creates a new generator.
func NewGenerator(s *Schema) *Generator {
	return &Generator{
		Schema:  s,
		defBufs: make(map[string]*bytes.Buffer),
	}
}

// GenerateAll generates all Go code and returns a map of filename -> content.
func (g *Generator) GenerateAll() map[string]string {
	// Collect all top-level definitions that need to be emitted
	var topLevel []string
	for name := range g.Schema.Defs {
		def := g.Schema.Defs[name]
		// ProtocolVersion is defined in meta.go
		if name == "ProtocolVersion" {
			continue
		}
		if def.Kind == DefKindEnum || def.Kind == DefKindObject || def.Kind == DefKindUnion || def.Kind == DefKindSimple {
			topLevel = append(topLevel, name)
		}
	}
	sort.Strings(topLevel)

	// First pass: generate each definition to its own buffer
	for _, name := range topLevel {
		g.defBufs[name] = &bytes.Buffer{}
		g.emitDefToBuf(name, g.defBufs[name])
	}

	// Always emit Meta type (referenced by _meta fields but not in schema.json)
	g.defBufs["Meta"] = &bytes.Buffer{}
	fmt.Fprintf(g.defBufs["Meta"], "// Meta is a map of arbitrary metadata attached to ACP messages.\n")
	fmt.Fprintf(g.defBufs["Meta"], "type Meta = map[string]any\n")

	// Second pass: group by file
	files := make(map[string][]string)
	for name := range g.defBufs {
		file := FileAssignment[name]
		if file == "" {
			file = DefaultFile
		}
		files[file] = append(files[file], name)
	}
	for f := range files {
		sort.Strings(files[f])
	}

	// Build file content
	results := make(map[string]string)

	// Add helpers file
	results["helpers.go"] = helpersContent

	for file, defNames := range files {
		var buf bytes.Buffer
		g.writeFileHeader(&buf, file)
		for _, name := range defNames {
			buf.Write(g.defBufs[name].Bytes())
		}
		results[file] = buf.String()
	}

	return results
}

// fileImports maps filename to its required import packages.
var fileImports = map[string][]string{
	"content.go":   {"encoding/json", "fmt"},
	"session.go":   {"encoding/json", "fmt"},
	"tool_call.go": {"encoding/json", "fmt"},
	"types.go":     {"encoding/json", "fmt"},
}

func (g *Generator) writeFileHeader(w *bytes.Buffer, filename string) {
	fmt.Fprintf(w, "// Code generated by internal/schemagen. DO NOT EDIT.\n\n")
	fmt.Fprintf(w, "package schema\n\n")
	if imports, ok := fileImports[filename]; ok && len(imports) > 0 {
		fmt.Fprintf(w, "import (\n")
		for _, imp := range imports {
			fmt.Fprintf(w, "\t%q\n", imp)
		}
		fmt.Fprintf(w, ")")
	}
}

// emitDefToBuf emits a definition to the specified buffer.
func (g *Generator) emitDefToBuf(name string, buf *bytes.Buffer) {
	def := g.Schema.Defs[name]
	if def == nil {
		return
	}

	// Add blank line before this item (except at very start of file).
	if buf.Len() > 0 {
		buf.WriteString("\n")
	}

	switch def.Kind {
	case DefKindEnum:
		g.emitEnumToBuf(def, buf)
	case DefKindUnion:
		g.emitUnionToBuf(def, buf)
	case DefKindObject:
		g.emitObjectToBuf(def, buf)
	case DefKindSimple:
		g.emitSimpleTypeToBuf(def, buf)
	}
}

// emitSimpleTypeToBuf generates a type alias (e.g. type SessionId = string).
func (g *Generator) emitSimpleTypeToBuf(def *Def, buf *bytes.Buffer) {
	// additionalProperties: true → map[string]any
	if def.AdditionalPropertiesAny {
		buf.WriteString("\n")
		if def.Description != "" {
			g.writeComment(def.Description, buf)
		}
		typeName := ToGoTypeName(def.Name)
		fmt.Fprintf(buf, "type %s = map[string]any\n", typeName)
		return
	}

	if def.Type == "" || def.Type == "object" {
		// anyOf with no refs (e.g. RequestId: null|int|string) → type X = any
		typeName := ToGoTypeName(def.Name)
		buf.WriteString("\n")
		fmt.Fprintf(buf, "type %s = any\n", typeName)
		return
	}

	buf.WriteString("\n")
	if def.Description != "" {
		g.writeComment(def.Description, buf)
	}

	typeName := ToGoTypeName(def.Name)
	goType := schemaTypeToGoType(def.Type)

	// Add a description comment
	desc := def.Description
	if desc != "" {
		firstLine := strings.Split(desc, "\n")[0]
		firstLine = strings.TrimRight(firstLine, ".")
		if firstLine != "" {
			fmt.Fprintf(buf, "// %s\n", firstLine)
		}
	}
	fmt.Fprintf(buf, "type %s = %s\n", typeName, goType)
}

// emitEnumToBuf generates enum type + const definitions.
func (g *Generator) emitEnumToBuf(def *Def, buf *bytes.Buffer) {
	var values []string
	if len(def.EnumValues) > 0 {
		values = def.EnumValues
	} else if len(def.OneOfConst) > 0 {
		values = def.OneOfConst
	} else if len(def.AnyOfEnumVals) > 0 {
		values = def.AnyOfEnumVals
	}
	if len(values) == 0 {
		return
	}

	buf.WriteString("\n")
	if def.Description != "" {
		g.writeComment(def.Description, buf)
	}

	typeName := ToGoTypeName(def.Name)
	fmt.Fprintf(buf, "// %s defines the values for %s.\n", typeName, typeName)
	fmt.Fprintf(buf, "type %s string\n\n", typeName)

	fmt.Fprintf(buf, "const (\n")
	sort.Slice(values, func(i, j int) bool {
		return ToGoConstName(typeName, values[i]) < ToGoConstName(typeName, values[j])
	})
	for _, v := range values {
		constName := ToGoConstName(typeName, v)
		fmt.Fprintf(buf, "\t%s %s = %q\n", constName, typeName, v)
	}
	fmt.Fprintf(buf, ")\n")
}

// emitObjectToBuf generates a struct definition.
func (g *Generator) emitObjectToBuf(def *Def, buf *bytes.Buffer) {
	typeName := ToGoTypeName(def.Name)

	// Check for inheritance (allOf with a base type)
	var baseProps []*Property
	var additionalProps []*Property
	seen := make(map[string]bool)

	if def.Extends != nil {
		base := g.Schema.Defs[def.Extends.Name]
		if base != nil && base.Kind == DefKindObject {
			baseProps = g.getAllProperties(base)
			for _, p := range baseProps {
				seen[p.Name] = true
			}
		}
	}

	// Collect additional properties (not in base)
	for _, p := range g.getAllProperties(def) {
		if !seen[p.Name] {
			additionalProps = append(additionalProps, p)
			seen[p.Name] = true
		}
	}

	// Only emit if it has content
	if len(baseProps) == 0 && len(additionalProps) == 0 {
		// anyOf with no refs (e.g. RequestId: null|int|string) → type X = any
		if def.Type == "" || def.Type == "object" {
			buf.WriteString("\n")
			fmt.Fprintf(buf, "type %s = any\n", typeName)
		}
		return
	}

	buf.WriteString("\n")
	if def.Description != "" {
		g.writeComment(def.Description, buf)
	}

	// For types that are just a base with no additions, emit the base inline
	// For types with additions, emit as embedding or plain struct
	if def.Extends != nil && len(additionalProps) == 0 {
		// Just aliasing or embedding
		return
	}

	// Build struct fields
	allProps := append(baseProps, additionalProps...)
	if len(allProps) == 0 {
		return
	}

	// Title from name
	fmt.Fprintf(buf, "// %s represents a [schema] type.\n", typeName)
	fmt.Fprintf(buf, "type %s struct {\n", typeName)

	// Embed base type if it exists
	if def.Extends != nil && len(baseProps) > 0 {
		baseName := ToGoTypeName(def.Extends.Name)
		fmt.Fprintf(buf, "\t%s\n", baseName)
	}

	// Additional properties (sorted alphabetically for stable output)
	sort.Slice(additionalProps, func(i, j int) bool {
		return additionalProps[i].Name < additionalProps[j].Name
	})
	for _, prop := range additionalProps {
		g.emitPropertyToBuf(prop, buf, def.Required)
	}

	fmt.Fprintf(buf, "}\n")
}

// discrimEnumTypeName returns the generated enum type name for a union's discriminator field.
// e.g. "type" + "ContentBlock" -> "ContentBlockTypeKind", "sessionUpdate" -> "SessionUpdateKind"
func discrimEnumTypeName(discField, parentName string) string {
	fieldName := ToGoTypeName(discField)
	// Always capitalize first letter to produce a valid Go identifier.
	result := string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:]
	if discField == "type" {
		// "type" is generic; prefix with parent union name to avoid collisions.
		return result + parentName + "Kind"
	}
	return result + "Kind"
}

// discrimEnumVals extracts unique discriminator values from a union's variants.
func discrimEnumVals(def *Def) (vals []string) {
	seen := make(map[string]bool)
	for _, v := range def.Variants {
		if v.DiscrimVal != "" && !seen[v.DiscrimVal] {
			seen[v.DiscrimVal] = true
			vals = append(vals, v.DiscrimVal)
		}
	}
	sort.Strings(vals)
	return vals
}

// emitUnionToBuf generates a discriminated union type.
func (g *Generator) emitUnionToBuf(def *Def, buf *bytes.Buffer) {
	typeName := ToGoTypeName(def.Name)
	discField := def.Discriminator
	if discField == "" {
		discField = "type"
	}
	discrimEnum := discrimEnumTypeName(discField, typeName)
	discVals := discrimEnumVals(def)

	buf.WriteString("\n")
	if def.Description != "" {
		g.writeComment(def.Description, buf)
	}

	// Emit the discriminator enum type if we have values
	if len(discVals) > 0 {
		fmt.Fprintf(buf, "// %s defines the values for %s.\n", discrimEnum, discrimEnum)
		fmt.Fprintf(buf, "type %s string\n\n", discrimEnum)
		fmt.Fprintf(buf, "const (\n")
		for _, dv := range discVals {
			// Use the variant's field name to build the constant name:
			// value="user_message_chunk" -> variant fieldName="UserMessageChunk"
			// const name = SessionUpdateKind + UserMessageChunk = SessionUpdateKindUserMessageChunk
			var variantFieldName string
			for _, variant := range def.Variants {
				if variant.DiscrimVal == dv {
					variantFieldName = variant.FieldName
					break
				}
			}
			if variantFieldName == "" {
				variantFieldName = DiscrimToFieldName(dv)
			}
			constName := discrimEnum + variantFieldName
			fmt.Fprintf(buf, "\t%s %s = %q\n", constName, discrimEnum, dv)
		}
		fmt.Fprintf(buf, ")\n\n")
	}

	fmt.Fprintf(buf, "// %s is a discriminated union.\n", typeName)
	fmt.Fprintf(buf, "type %s struct {\n", typeName)

	// Discriminator field
	fieldName := ToGoFieldName(discField)
	if len(discVals) > 0 {
		fmt.Fprintf(buf, "\t%s %s `json:%q`\n", fieldName, discrimEnum, discField)
	} else {
		fmt.Fprintf(buf, "\t%s string `json:%q`\n", fieldName, discField)
	}

	// Variant fields
	for _, v := range def.Variants {
		if v.Ref == nil {
			continue
		}
		// Recursively emit the sub-type to its own buffer first
		subName := v.Ref.Name
		if subBuf, ok := g.defBufs[subName]; ok && subBuf.Len() == 0 {
			// Not yet generated - generate it now
			g.defBufs[subName] = &bytes.Buffer{}
			g.emitDefToBuf(subName, g.defBufs[subName])
		}

		variantFieldName := v.FieldName
		if variantFieldName == "" {
			variantFieldName = ToGoTypeName(subName)
		}
		subTypeName := ToGoTypeName(subName)
		fmt.Fprintf(buf, "\t%s *%s `json:\"-\"`\n", variantFieldName, subTypeName)
	}

	fmt.Fprintf(buf, "}\n")

	// MarshalJSON
	g.emitUnionMarshalJSON(def, typeName, discField, discVals, buf)

	// UnmarshalJSON
	g.emitUnionUnmarshalJSON(def, typeName, discField, discVals, buf)
}

func (g *Generator) emitUnionMarshalJSON(def *Def, typeName, discField string, discVals []string, buf *bytes.Buffer) {
	hasEnum := len(discVals) > 0
	fieldName := ToGoFieldName(discField)
	discrimEnum := discrimEnumTypeName(discField, typeName)
	fmt.Fprintf(buf, "\nfunc (u %s) MarshalJSON() ([]byte, error) {\n", typeName)
	fmt.Fprintf(buf, "\tswitch u.%s {\n", fieldName)
	// Use discVals order (sorted) to match the enum constant declaration
	for _, dv := range discVals {
		variantFieldName := ""
		for _, v := range def.Variants {
			if v.DiscrimVal == dv {
				variantFieldName = v.FieldName
				break
			}
		}
		if variantFieldName == "" {
			variantFieldName = DiscrimToFieldName(dv)
		}
		if hasEnum {
			fmt.Fprintf(buf, "\tcase %s%s:\n", discrimEnum, variantFieldName)
		} else {
			fmt.Fprintf(buf, "\tcase %q:\n", dv)
		}
		fmt.Fprintf(buf, "\t\treturn marshalWith(u.%s, %q, %q)\n", variantFieldName, discField, dv)
	}
	fmt.Fprintf(buf, "\tdefault:\n")
	if hasEnum {
		fmt.Fprintf(buf, "\t\treturn json.Marshal(map[string]%s{%q: u.%s})\n", discrimEnum, discField, fieldName)
	} else {
		fmt.Fprintf(buf, "\t\treturn json.Marshal(map[string]string{%q: u.%s})\n", discField, fieldName)
	}
	fmt.Fprintf(buf, "\t}\n")
	fmt.Fprintf(buf, "}\n")
}

func (g *Generator) emitUnionUnmarshalJSON(def *Def, typeName, discField string, discVals []string, buf *bytes.Buffer) {
	hasEnum := len(discVals) > 0
	fieldName := ToGoFieldName(discField)
	discrimEnum := discrimEnumTypeName(discField, typeName)
	fmt.Fprintf(buf, "\nfunc (u *%s) UnmarshalJSON(data []byte) error {\n", typeName)
	fmt.Fprintf(buf, "\tvar raw map[string]json.RawMessage\n")
	fmt.Fprintf(buf, "\tif err := json.Unmarshal(data, &raw); err != nil {\n")
	fmt.Fprintf(buf, "\t\treturn err\n")
	fmt.Fprintf(buf, "\t}\n")
	fmt.Fprintf(buf, "\tdiscrim, ok := raw[%q]\n", discField)
	fmt.Fprintf(buf, "\tif !ok {\n")
	fmt.Fprintf(buf, "\t\treturn fmt.Errorf(%q)\n", "missing "+discField+" field")
	fmt.Fprintf(buf, "\t}\n")
	fmt.Fprintf(buf, "\tvar dv string\n")
	fmt.Fprintf(buf, "\tif err := json.Unmarshal(discrim, &dv); err != nil {\n")
	fmt.Fprintf(buf, "\t\treturn err\n")
	fmt.Fprintf(buf, "\t}\n")
	if hasEnum {
		fmt.Fprintf(buf, "\tu.%s = %s(dv)\n", fieldName, discrimEnum)
	} else {
		fmt.Fprintf(buf, "\tu.%s = dv\n", fieldName)
	}
	fmt.Fprintf(buf, "\tswitch u.%s {\n", fieldName)
	for _, dv := range discVals {
		variantFieldName := ""
		var subType string
		for _, v := range def.Variants {
			if v.DiscrimVal == dv && v.Ref != nil {
				variantFieldName = v.FieldName
				if variantFieldName == "" {
					variantFieldName = ToGoTypeName(v.Ref.Name)
				}
				subType = ToGoTypeName(v.Ref.Name)
				break
			}
		}
		if variantFieldName == "" {
			continue
		}
		if hasEnum {
			fmt.Fprintf(buf, "\tcase %s%s:\n", discrimEnum, variantFieldName)
		} else {
			fmt.Fprintf(buf, "\tcase %q:\n", dv)
		}
		fmt.Fprintf(buf, "\t\tu.%s = &%s{}\n", variantFieldName, subType)
		fmt.Fprintf(buf, "\t\treturn json.Unmarshal(data, u.%s)\n", variantFieldName)
	}
	fmt.Fprintf(buf, "\tdefault:\n")
	fmt.Fprintf(buf, "\t\treturn nil\n")
	fmt.Fprintf(buf, "\t}\n")
	fmt.Fprintf(buf, "}\n")
}

func (g *Generator) getAllProperties(def *Def) []*Property {
	if def == nil {
		return nil
	}
	props := make([]*Property, 0)
	seen := make(map[string]bool)

	// Get base properties first
	if def.Extends != nil {
		base := g.Schema.Defs[def.Extends.Name]
		for _, p := range g.getAllProperties(base) {
			if !seen[p.Name] {
				seen[p.Name] = true
				props = append(props, p)
			}
		}
	}

	// Add own properties
	for _, p := range def.Properties {
		if !seen[p.Name] {
			seen[p.Name] = true
			props = append(props, p)
		}
	}
	return props
}

func (g *Generator) emitPropertyToBuf(prop *Property, buf *bytes.Buffer, required []string) {
	fieldName := ToGoFieldName(prop.Name)
	// _meta must be exported as Meta so the json tag is valid (encoding/json
	// cannot serialize unexported fields, so _meta + json tag triggers go vet).
	if prop.Name == "_meta" {
		fieldName = "Meta"
	}
	goType := g.propertyGoType(prop)
	isRequired := false
	for _, r := range required {
		if r == prop.Name {
			isRequired = true
			break
		}
	}
	jsonTag := fmt.Sprintf("`json:\"%s", prop.Name)
	if !isRequired {
		jsonTag += ",omitempty"
	}
	jsonTag += "\"`"

	if prop.Description != "" {
		lines := strings.Split(strings.TrimSpace(prop.Description), "\n")
		if lines[0] != "" {
			fmt.Fprintf(buf, "\t// %s\n", strings.TrimRight(lines[0], "."))
		}
	}

	fmt.Fprintf(buf, "\t%s %s %s\n", fieldName, goType, jsonTag)
}

func (g *Generator) propertyGoType(prop *Property) string {
	tr := prop.Type
	if tr == nil {
		return "any"
	}

	// Special case: _meta field
	if prop.Name == "_meta" {
		return "Meta"
	}

	if tr.IsAny {
		return "any"
	}
	if tr.IsSlice {
		itemType := g.typeRefGoType(tr.ArrayItem)
		return "[]" + itemType
	}
	if tr.IsMap {
		valType := g.typeRefGoType(tr.MapValue)
		return "map[string]" + valType
	}
	return g.typeRefGoType(tr)
}

func (g *Generator) typeRefGoType(tr *TypeRef) string {
	if tr == nil {
		return "any"
	}
	if tr.IsAny {
		return "any"
	}
	goType := tr.GoType
	if goType == "" || goType == "object" {
		return "any"
	}
	// Map JSON Schema types to Go types
	switch goType {
	case "integer":
		goType = "int"
	case "number":
		goType = "float64"
	case "boolean":
		goType = "bool"
	case "string":
		goType = "string"
	}
	if isGoBuiltIn(goType) {
		if tr.IsPtr {
			return "*" + goType
		}
		return goType
	}
	goType = ToGoTypeName(goType)
	if tr.IsPtr {
		return "*" + goType
	}
	return goType
}

func (g *Generator) writeComment(s string, buf *bytes.Buffer) {
	s = strings.TrimRight(s, ".")
	lines := strings.Split(s, "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line != "" {
			fmt.Fprintf(buf, "// %s\n", line)
		}
	}
}

// ToGoTypeName converts a schema name to a Go type name.
func ToGoTypeName(name string) string {
	if override, ok := NameOverrides[name]; ok {
		return override
	}
	return name
}

// ToGoFieldName converts a JSON property name to a Go field name.
// Only capitalizes the first letter; no other rename logic.
func ToGoFieldName(name string) string {
	if name == "" {
		return name
	}
	return string(unicode.ToUpper(rune(name[0]))) + name[1:]
}

// ToGoConstName converts an enum value to a Go constant name.
func ToGoConstName(typeName, value string) string {
	// Convert value to PascalCase
	valueName := value
	if strings.Contains(value, "_") {
		words := strings.Split(value, "_")
		for i, w := range words {
			if len(w) > 0 {
				words[i] = string(unicode.ToUpper(rune(w[0]))) + w[1:]
			}
		}
		valueName = strings.Join(words, "")
	} else {
		valueName = string(unicode.ToUpper(rune(value[0]))) + value[1:]
	}
	return typeName + valueName
}

func isGoBuiltIn(t string) bool {
	switch t {
	case "string", "bool", "int", "int8", "int16", "int32", "int64",
		"uint", "uint8", "uint16", "uint32", "uint64",
		"float32", "float64", "byte", "rune", "uintptr", "any", "error":
		return true
	default:
		return false
	}
}

// schemaTypeToGoType maps JSON Schema types to Go types.
func schemaTypeToGoType(t string) string {
	switch t {
	case "string":
		return "string"
	case "integer":
		return "int"
	case "number":
		return "float64"
	case "boolean":
		return "bool"
	case "object":
		return "any"
	default:
		return "any"
	}
}

// helpersContent is the generated helpers file content.
const helpersContent = `// Code generated by internal/schemagen. DO NOT EDIT.

package schema

import (
	"encoding/json"
	"fmt"
)

// marshalWith marshals v and injects a key-value pair.
func marshalWith(v interface{}, key, value string) ([]byte, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	return injectField(b, key, value)
}

// injectField injects a key-value pair into a JSON object byte slice.
func injectField(data []byte, key, value string) ([]byte, error) {
	var obj map[string]json.RawMessage
	if err := json.Unmarshal(data, &obj); err != nil {
		return nil, err
	}
	v, _ := json.Marshal(value)
	obj[key] = v
	return json.Marshal(obj)
}

// errMissingField creates a descriptive error for a missing JSON field.
func errMissingField(field, typeName string) error {
	return fmt.Errorf("missing required field %q in %s", field, typeName)
}
`

var wordBoundary = regexp.MustCompile(`[_\-]+`)
