🏡 index : quine-global/quintodrome.git

package internal

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"maps"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strings"
)

// Annotation patterns
var (
	// //nd:hostservice name=ServiceName permission=key
	hostServicePattern = regexp.MustCompile(`//nd:hostservice\s+(.*)`)
	// //nd:hostfunc [name=CustomName]
	hostFuncPattern = regexp.MustCompile(`//nd:hostfunc(?:\s+(.*))?`)
	// //nd:capability name=PackageName [required=true]
	capabilityPattern = regexp.MustCompile(`//nd:capability\s+(.*)`)
	// //nd:export name=ExportName
	exportPattern = regexp.MustCompile(`//nd:export\s+(.*)`)
	// key=value pairs
	keyValuePattern = regexp.MustCompile(`(\w+)=(\S+)`)
)

// parsedGoFile pairs a source path with its already-parsed AST.
type parsedGoFile struct {
	path string
	file *ast.File
}

// parseGoFiles returns the eligible Go source files in dir, each parsed once.
func parseGoFiles(dir string, fset *token.FileSet) ([]parsedGoFile, error) {
	paths, err := goSourceFiles(dir)
	if err != nil {
		return nil, err
	}
	out := make([]parsedGoFile, 0, len(paths))
	for _, p := range paths {
		f, err := parser.ParseFile(fset, p, nil, parser.ParseComments)
		if err != nil {
			return nil, fmt.Errorf("parsing %s: %w", filepath.Base(p), err)
		}
		out = append(out, parsedGoFile{path: p, file: f})
	}
	return out, nil
}

// goSourceFiles returns the Go source file paths in dir, excluding generated,
// test, and doc files.
func goSourceFiles(dir string) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("reading directory: %w", err)
	}
	var paths []string
	for _, entry := range entries {
		name := entry.Name()
		if entry.IsDir() || !strings.HasSuffix(name, ".go") {
			continue
		}
		if strings.HasSuffix(name, "_gen.go") || strings.HasSuffix(name, "_test.go") || name == "doc.go" {
			continue
		}
		paths = append(paths, filepath.Join(dir, name))
	}
	return paths, nil
}

// ParseDirectory parses all Go source files in a directory and extracts host services.
func ParseDirectory(dir string) ([]Service, error) {
	return ParseDirectoryWithShared(dir, nil)
}

// ParseDirectoryWithShared parses all Go source files in a directory, resolving any
// type aliases that reference the shared `types` package against the provided registry.
func ParseDirectoryWithShared(dir string, shared map[string]StructDef) ([]Service, error) {
	fset := token.NewFileSet()
	parsed, err := parseGoFiles(dir, fset)
	if err != nil {
		return nil, err
	}

	// First pass: collect all struct definitions and type aliases from every file
	// so that a struct or alias declared in one file is visible when resolving
	// types in a sibling file.
	pkgStructMap := make(map[string]StructDef)
	pkgAliasMap := make(map[string]TypeAlias)
	for _, pf := range parsed {
		for _, s := range parseStructs(pf.file) {
			pkgStructMap[s.Name] = s
		}
		for _, a := range parseTypeAliases(pf.file) {
			pkgAliasMap[a.Name] = a
		}
	}

	// Second pass: parse services using the package-level maps.
	var services []Service
	for _, pf := range parsed {
		svcList, err := parseServiceFile(pf.file, pkgStructMap, pkgAliasMap, shared)
		if err != nil {
			return nil, fmt.Errorf("parsing %s: %w", filepath.Base(pf.path), err)
		}
		services = append(services, svcList...)
	}

	return services, nil
}

// ParseCapabilities parses all Go source files in a directory and extracts capabilities.
func ParseCapabilities(dir string) ([]Capability, error) {
	return ParseCapabilitiesWithShared(dir, nil)
}

// ParseCapabilitiesWithShared parses all Go source files in a directory, resolving any
// type aliases that reference the shared `types` package against the provided registry.
func ParseCapabilitiesWithShared(dir string, shared map[string]StructDef) ([]Capability, error) {
	fset := token.NewFileSet()
	parsed, err := parseGoFiles(dir, fset)
	if err != nil {
		return nil, err
	}

	// First pass: collect all structs, type aliases, and const groups.
	pkgStructMap := make(map[string]StructDef)
	pkgAliasMap := make(map[string]TypeAlias)
	var allConstGroups []ConstGroup

	for _, pf := range parsed {
		for _, s := range parseStructs(pf.file) {
			pkgStructMap[s.Name] = s
		}
		for _, a := range parseTypeAliases(pf.file) {
			pkgAliasMap[a.Name] = a
		}
		allConstGroups = append(allConstGroups, parseConstGroups(pf.file)...)
	}

	// Second pass: parse capabilities using the package-level type maps.
	var capabilities []Capability
	for _, pf := range parsed {
		capList, err := parseCapabilityFile(pf.path, pf.file, pkgStructMap, pkgAliasMap, allConstGroups, shared)
		if err != nil {
			return nil, fmt.Errorf("parsing %s: %w", filepath.Base(pf.path), err)
		}
		capabilities = append(capabilities, capList...)
	}

	return capabilities, nil
}

// LoadSharedTypes parses every struct defined in dir (the shared `types` source
// package) and returns them keyed by name. dir == "" yields an empty map.
func LoadSharedTypes(dir string) (map[string]StructDef, error) {
	result := map[string]StructDef{}
	if dir == "" {
		return result, nil
	}
	paths, err := goSourceFiles(dir)
	if err != nil {
		return nil, fmt.Errorf("reading shared types directory: %w", err)
	}
	fset := token.NewFileSet()
	for _, path := range paths {
		f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
		if err != nil {
			return nil, fmt.Errorf("parsing %s: %w", filepath.Base(path), err)
		}
		for _, s := range parseStructs(f) {
			result[s.Name] = s
		}
	}
	return result, nil
}

// parseCapabilityFile parses a single Go source file and extracts capabilities.
func parseCapabilityFile(path string, f *ast.File, structMap map[string]StructDef, aliasMap map[string]TypeAlias, allConstGroups []ConstGroup, shared map[string]StructDef) ([]Capability, error) {
	var capabilities []Capability

	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
			if !ok {
				continue
			}

			// Check for //nd:capability annotation in doc comment
			docText, rawDoc := getDocComment(genDecl, typeSpec)
			capAnnotation := parseCapabilityAnnotation(rawDoc)
			if capAnnotation == nil {
				continue
			}

			// Extract source file base name (e.g., "websocket_callback" from "websocket_callback.go")
			baseName := filepath.Base(path)
			sourceFile := strings.TrimSuffix(baseName, ".go")

			capability := Capability{
				Name:       capAnnotation["name"],
				Interface:  typeSpec.Name.Name,
				Required:   capAnnotation["required"] == "true",
				Doc:        cleanDoc(docText),
				SourceFile: sourceFile,
			}

			// Parse methods and collect referenced types
			referencedTypes := make(map[string]bool)
			for _, method := range interfaceType.Methods.List {
				if len(method.Names) == 0 {
					continue // Embedded interface
				}

				funcType, ok := method.Type.(*ast.FuncType)
				if !ok {
					continue
				}

				// Check for //nd:export annotation
				methodDocText, methodRawDoc := getMethodDocComment(method)
				exportAnnotation := parseExportAnnotation(methodRawDoc)
				if exportAnnotation == nil {
					continue
				}

				export, err := parseExport(method.Names[0].Name, funcType, exportAnnotation, cleanDoc(methodDocText))
				if err != nil {
					return nil, fmt.Errorf("parsing export %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err)
				}
				capability.Methods = append(capability.Methods, export)

				// Collect referenced types from input and output
				collectReferencedTypes(export.Input.Type, referencedTypes)
				collectReferencedTypes(export.Output.Type, referencedTypes)
			}

			// Recursively collect all struct dependencies
			collectAllStructDependencies(referencedTypes, structMap)

			// Resolve shared-type aliases against the registry
			sharedAliases, sharedTypes, err := resolveSharedAliases(referencedTypes, aliasMap, shared)
			if err != nil {
				return nil, err
			}
			capability.SharedAliases = sharedAliases
			capability.SharedTypes = sharedTypes

			// Build a set of names already covered by SharedAliases so we don't
			// emit them again in TypeAliases (which would cause a redeclaration).
			sharedAliasNames := make(map[string]bool, len(capability.SharedAliases))
			for _, sa := range capability.SharedAliases {
				sharedAliasNames[sa.Name] = true
			}

			// Sort type names for stable output order
			sortedTypeNames := slices.Sorted(maps.Keys(referencedTypes))

			// Attach referenced structs to the capability
			for _, typeName := range sortedTypeNames {
				if s, exists := structMap[typeName]; exists {
					capability.Structs = append(capability.Structs, s)
				}
			}

			// Attach referenced type aliases (skip those already in SharedAliases)
			for _, typeName := range sortedTypeNames {
				if sharedAliasNames[typeName] {
					continue
				}
				if a, exists := aliasMap[typeName]; exists {
					capability.TypeAliases = append(capability.TypeAliases, a)
				}
			}

			// Also attach type aliases prefixed with interface name (e.g., ScrobblerError for Scrobbler interface)
			// This supports error types that are not directly referenced in method signatures
			interfaceName := typeSpec.Name.Name
			for _, typeName := range slices.Sorted(maps.Keys(aliasMap)) {
				if sharedAliasNames[typeName] {
					continue
				}
				a := aliasMap[typeName]
				if strings.HasPrefix(typeName, interfaceName) && !referencedTypes[typeName] {
					capability.TypeAliases = append(capability.TypeAliases, a)
					referencedTypes[typeName] = true // Mark as referenced for const lookup
				}
			}

			// Attach const groups that match referenced type aliases
			for _, group := range allConstGroups {
				if group.Type == "" {
					continue
				}
				if referencedTypes[group.Type] {
					capability.Consts = append(capability.Consts, group)
				}
			}

			if len(capability.Methods) > 0 {
				capabilities = append(capabilities, capability)
			}
		}
	}

	return capabilities, nil
}

// resolveSharedAliases determines which shared `types` package structs a host
// service or capability uses and returns the deprecated re-export aliases to emit
// for them.
//
// A shared type counts as used when a field references it by qualified name
// (e.g. types.Track) or via a declared alias used by bare name (e.g. a field of
// type TrackInfo where `type TrackInfo = types.Track`). The shared struct's own
// fields are followed transitively so nested shared types are picked up too. For
// every used canonical type, each declared `type X = types.Canonical` alias is
// emitted as a SharedAlias so the generated PDK keeps re-exporting it for
// backwards compatibility.
//
// It returns the deprecated re-export aliases to emit and the resolved shapes of
// every used shared type (alias or not, for schema inlining).
//
// Returns an error if a referenced shared type cannot be found in the shared registry.
func resolveSharedAliases(referenced map[string]bool, aliasMap map[string]TypeAlias, shared map[string]StructDef) ([]SharedAlias, []StructDef, error) {
	// Index declared shared aliases by the canonical type they target, e.g.
	// "Track" -> [TrackInfo]. A canonical type may have more than one alias.
	aliasesByCanonical := map[string][]TypeAlias{}
	for _, a := range aliasMap {
		if a.IsSharedAlias() {
			canonical := strings.TrimPrefix(a.Type, sharedTypesPrefix)
			aliasesByCanonical[canonical] = append(aliasesByCanonical[canonical], a)
		}
	}

	// Walk the referenced types, following nested shared references inside the
	// shared structs, to find the set of canonical shared types used.
	used := map[string]bool{}
	var queue []string
	for name := range referenced {
		if c, ok := seedSharedCanonical(name, aliasMap); ok {
			queue = append(queue, c)
		}
	}
	fieldRefs := map[string]bool{}
	for len(queue) > 0 {
		canonical := queue[0]
		queue = queue[1:]
		if used[canonical] {
			continue
		}
		def, ok := shared[canonical]
		if !ok {
			return nil, nil, fmt.Errorf(
				"shared type %q could not be resolved: pass -shared=<dir> pointing at the shared types package, and ensure %s is defined there",
				canonical, sharedTypesPrefix+canonical,
			)
		}
		used[canonical] = true
		for _, f := range def.Fields {
			clear(fieldRefs)
			collectReferencedTypes(f.Type, fieldRefs)
			for t := range fieldRefs {
				if c, ok := nestedSharedCanonical(t, shared); ok {
					queue = append(queue, c)
				}
			}
		}
	}

	var out []SharedAlias
	var usedDefs []StructDef
	for canonical := range used {
		usedDefs = append(usedDefs, shared[canonical])
		for _, a := range aliasesByCanonical[canonical] {
			out = append(out, SharedAlias{Name: a.Name, Target: a.Type, Doc: a.Doc, Def: shared[canonical]})
		}
	}
	slices.SortFunc(out, func(a, b SharedAlias) int { return strings.Compare(a.Name, b.Name) })
	slices.SortFunc(usedDefs, func(a, b StructDef) int { return strings.Compare(a.Name, b.Name) })
	return out, usedDefs, nil
}

// seedSharedCanonical maps a type token referenced by a capability/service field
// to the canonical shared type it denotes. It recognizes qualified references
// (types.X -> X) and declared shared aliases used by bare name (X where
// `type X = types.Y` -> Y). A bare name that is not a declared shared alias is
// not treated as shared, so a local struct sharing a name with a shared type is
// never misclassified.
func seedSharedCanonical(name string, aliasMap map[string]TypeAlias) (string, bool) {
	if rest, ok := strings.CutPrefix(name, sharedTypesPrefix); ok {
		return rest, true
	}
	if a, ok := aliasMap[name]; ok && a.IsSharedAlias() {
		return strings.TrimPrefix(a.Type, sharedTypesPrefix), true
	}
	return "", false
}

// nestedSharedCanonical maps a type token found inside a shared struct's own
// fields to a canonical shared type. Within the shared package, types reference
// each other by bare name (e.g. Track.Artists is []ArtistRef), so any bare name
// present in the shared registry counts.
func nestedSharedCanonical(name string, shared map[string]StructDef) (string, bool) {
	if rest, ok := strings.CutPrefix(name, sharedTypesPrefix); ok {
		return rest, true
	}
	if _, ok := shared[name]; ok {
		return name, true
	}
	return "", false
}

// collectAllStructDependencies recursively collects all struct types referenced by other structs.
func collectAllStructDependencies(referencedTypes map[string]bool, structMap map[string]StructDef) {
	// Keep iterating until no new types are added
	for {
		newTypes := make(map[string]bool)
		for typeName := range referencedTypes {
			if s, exists := structMap[typeName]; exists {
				for _, field := range s.Fields {
					collectReferencedTypes(field.Type, newTypes)
				}
			}
		}
		// Check if any new types were found
		foundNew := false
		for t := range newTypes {
			if !referencedTypes[t] {
				referencedTypes[t] = true
				foundNew = true
			}
		}
		if !foundNew {
			break
		}
	}
}

// parseExport parses an export method signature into an Export struct.
func parseExport(name string, funcType *ast.FuncType, annotation map[string]string, doc string) (Export, error) {
	export := Export{
		Name:       name,
		ExportName: annotation["name"],
		Doc:        doc,
	}

	// Capability exports have exactly one input parameter (the struct type)
	if funcType.Params != nil && len(funcType.Params.List) == 1 {
		field := funcType.Params.List[0]
		typeName := typeToString(field.Type)
		paramName := "input"
		if len(field.Names) > 0 {
			paramName = field.Names[0].Name
		}
		export.Input = NewParam(paramName, typeName)
	}

	// Capability exports return (OutputType, error)
	if funcType.Results != nil {
		for _, field := range funcType.Results.List {
			typeName := typeToString(field.Type)
			if typeName == "error" {
				continue // Skip error return
			}
			paramName := "output"
			if len(field.Names) > 0 {
				paramName = field.Names[0].Name
			}
			export.Output = NewParam(paramName, typeName)
			break // Only take the first non-error return
		}
	}

	return export, nil
}

// parseServiceFile parses a single Go source file and extracts host services.
// pkgStructMap and pkgAliasMap are the package-wide struct and alias maps built
// from all files in the package, so a host-service interface can reference types
// defined in a sibling file.
func parseServiceFile(f *ast.File, pkgStructMap map[string]StructDef, pkgAliasMap map[string]TypeAlias, shared map[string]StructDef) ([]Service, error) {
	var services []Service

	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
			if !ok {
				continue
			}

			// Check for //nd:hostservice annotation in doc comment
			docText, rawDoc := getDocComment(genDecl, typeSpec)
			svcAnnotation := parseHostServiceAnnotation(rawDoc)
			if svcAnnotation == nil {
				continue
			}

			service := Service{
				Name:       svcAnnotation["name"],
				Permission: svcAnnotation["permission"],
				Interface:  typeSpec.Name.Name,
				Doc:        cleanDoc(docText),
			}

			// Parse methods and collect referenced types
			referencedTypes := make(map[string]bool)
			for _, method := range interfaceType.Methods.List {
				if len(method.Names) == 0 {
					continue // Embedded interface
				}

				funcType, ok := method.Type.(*ast.FuncType)
				if !ok {
					continue
				}

				// Check for //nd:hostfunc annotation
				methodDocText, methodRawDoc := getMethodDocComment(method)
				methodAnnotation := parseHostFuncAnnotation(methodRawDoc)
				if methodAnnotation == nil {
					continue
				}

				m, err := parseMethod(method.Names[0].Name, funcType, methodAnnotation, cleanDoc(methodDocText))
				if err != nil {
					return nil, fmt.Errorf("parsing method %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err)
				}
				service.Methods = append(service.Methods, m)

				// Collect referenced types from params and returns
				for _, p := range m.Params {
					collectReferencedTypes(p.Type, referencedTypes)
				}
				for _, r := range m.Returns {
					collectReferencedTypes(r.Type, referencedTypes)
				}
			}

			// Resolve shared-type aliases against the registry. Host-service schemas
			// are not generated (the -schemas pass is capability-only), so the resolved
			// shared shapes are not needed here.
			sharedAliases, _, err := resolveSharedAliases(referencedTypes, pkgAliasMap, shared)
			if err != nil {
				return nil, err
			}
			service.SharedAliases = sharedAliases

			// Recursively collect all struct dependencies so types referenced only
			// transitively (e.g. a field type of a referenced struct) are attached.
			collectAllStructDependencies(referencedTypes, pkgStructMap)

			// Attach referenced structs to the service (sorted for stable output)
			for _, typeName := range slices.Sorted(maps.Keys(referencedTypes)) {
				if s, exists := pkgStructMap[typeName]; exists {
					service.Structs = append(service.Structs, s)
				}
			}

			if len(service.Methods) > 0 {
				services = append(services, service)
			}
		}
	}

	return services, nil
}

// parseStructs extracts all struct type definitions from a parsed Go file.
func parseStructs(f *ast.File) []StructDef {
	var structs []StructDef

	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}

			docText, _ := getDocComment(genDecl, typeSpec)
			s := StructDef{
				Name: typeSpec.Name.Name,
				Doc:  cleanDoc(docText),
			}

			// Parse struct fields
			for _, field := range structType.Fields.List {
				if len(field.Names) == 0 {
					continue // Embedded field
				}

				fieldDef := parseStructField(field)
				s.Fields = append(s.Fields, fieldDef...)
			}

			structs = append(structs, s)
		}
	}

	return structs
}

// parseTypeAliases extracts all type alias definitions from a parsed Go file.
// Type aliases are non-struct type declarations like: type MyType string
func parseTypeAliases(f *ast.File) []TypeAlias {
	var aliases []TypeAlias

	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			// Skip struct and interface types
			if _, isStruct := typeSpec.Type.(*ast.StructType); isStruct {
				continue
			}
			if _, isInterface := typeSpec.Type.(*ast.InterfaceType); isInterface {
				continue
			}

			docText, _ := getDocComment(genDecl, typeSpec)
			aliases = append(aliases, TypeAlias{
				Name:    typeSpec.Name.Name,
				Type:    typeToString(typeSpec.Type),
				Doc:     cleanDoc(docText),
				IsAlias: typeSpec.Assign.IsValid(),
			})
		}
	}

	return aliases
}

// parseConstGroups extracts const groups from a parsed Go file.
func parseConstGroups(f *ast.File) []ConstGroup {
	var groups []ConstGroup

	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.CONST {
			continue
		}

		group := ConstGroup{}
		for _, spec := range genDecl.Specs {
			valueSpec, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}

			// Get type if specified
			if valueSpec.Type != nil && group.Type == "" {
				group.Type = typeToString(valueSpec.Type)
			}

			// Extract values
			for i, name := range valueSpec.Names {
				def := ConstDef{
					Name: name.Name,
				}
				// Get value if present
				if i < len(valueSpec.Values) {
					def.Value = exprToString(valueSpec.Values[i])
				}
				// Get doc comment
				if valueSpec.Doc != nil {
					def.Doc = cleanDoc(valueSpec.Doc.Text())
				} else if valueSpec.Comment != nil {
					def.Doc = cleanDoc(valueSpec.Comment.Text())
				}
				group.Values = append(group.Values, def)
			}
		}

		if len(group.Values) > 0 {
			groups = append(groups, group)
		}
	}

	return groups
}

// exprToString converts an AST expression to a Go source string.
func exprToString(expr ast.Expr) string {
	switch e := expr.(type) {
	case *ast.BasicLit:
		return e.Value
	case *ast.Ident:
		return e.Name
	default:
		return ""
	}
}

// parseStructField parses a struct field and returns FieldDef for each name.
func parseStructField(field *ast.Field) []FieldDef {
	var fields []FieldDef
	typeName := typeToString(field.Type)

	// Parse struct tag for JSON field name and omitempty
	jsonTag := ""
	omitEmpty := false
	if field.Tag != nil {
		tag := field.Tag.Value
		// Remove backticks
		tag = strings.Trim(tag, "`")
		// Parse json tag
		jsonTag, omitEmpty = parseJSONTag(tag)
	}

	// Get doc comment
	var doc string
	if field.Doc != nil {
		doc = cleanDoc(field.Doc.Text())
	}

	for _, name := range field.Names {
		fieldJSONTag := jsonTag
		if fieldJSONTag == "" {
			// Default to field name with camelCase
			fieldJSONTag = toJSONName(name.Name)
		}
		fields = append(fields, FieldDef{
			Name:      name.Name,
			Type:      typeName,
			JSONTag:   fieldJSONTag,
			OmitEmpty: omitEmpty,
			Doc:       doc,
		})
	}

	return fields
}

// parseJSONTag extracts the json field name and omitempty flag from a struct tag.
func parseJSONTag(tag string) (name string, omitEmpty bool) {
	// Find json:"..." in the tag
	for _, part := range strings.Split(tag, " ") {
		if strings.HasPrefix(part, `json:"`) {
			value := strings.TrimPrefix(part, `json:"`)
			value = strings.TrimSuffix(value, `"`)
			parts := strings.Split(value, ",")
			if len(parts) > 0 && parts[0] != "-" {
				name = parts[0]
			}
			for _, opt := range parts[1:] {
				if opt == "omitempty" {
					omitEmpty = true
				}
			}
			return
		}
	}
	return "", false
}

// collectReferencedTypes extracts custom type names from a Go type string.
// It handles pointers, slices, and maps, collecting base type names.
func collectReferencedTypes(goType string, refs map[string]bool) {
	// Strip pointer
	if strings.HasPrefix(goType, "*") {
		collectReferencedTypes(goType[1:], refs)
		return
	}
	// Strip slice
	if strings.HasPrefix(goType, "[]") {
		if goType != "[]byte" {
			collectReferencedTypes(goType[2:], refs)
		}
		return
	}
	// Handle map
	if strings.HasPrefix(goType, "map[") {
		rest := goType[4:] // Remove "map["
		depth := 1
		keyEnd := 0
		for i, r := range rest {
			if r == '[' {
				depth++
			} else if r == ']' {
				depth--
				if depth == 0 {
					keyEnd = i
					break
				}
			}
		}
		keyType := rest[:keyEnd]
		valueType := rest[keyEnd+1:]
		collectReferencedTypes(keyType, refs)
		collectReferencedTypes(valueType, refs)
		return
	}

	// Qualified reference to the shared `types` package (e.g. types.Track).
	// These start with a lowercase package selector, so they must be collected
	// before the uppercase check below would skip them.
	if strings.HasPrefix(goType, sharedTypesPrefix) {
		refs[goType] = true
		return
	}

	// Check if it's a custom type (starts with uppercase, not a builtin)
	if len(goType) > 0 && goType[0] >= 'A' && goType[0] <= 'Z' {
		switch goType {
		case "String", "Bool", "Int", "Int32", "Int64", "Float32", "Float64":
			// Not custom types (just capitalized for some reason)
		default:
			refs[goType] = true
		}
	}
}

// toJSONName is imported from types.go via the same package

// getDocComment extracts the doc comment for a type spec.
// Returns both the readable doc text and the raw comment text (which includes pragma-style comments).
func getDocComment(genDecl *ast.GenDecl, typeSpec *ast.TypeSpec) (docText, rawText string) {
	var docGroup *ast.CommentGroup
	// First check the TypeSpec's own doc (when multiple types in one block)
	if typeSpec.Doc != nil {
		docGroup = typeSpec.Doc
	} else if genDecl.Doc != nil {
		// Fall back to GenDecl doc (single type declaration)
		docGroup = genDecl.Doc
	}
	if docGroup == nil {
		return "", ""
	}
	return docGroup.Text(), commentGroupRaw(docGroup)
}

// commentGroupRaw returns all comment text including pragma-style comments (//nd:...).
// Go's ast.CommentGroup.Text() strips comments without a space after //, so we need this.
func commentGroupRaw(cg *ast.CommentGroup) string {
	if cg == nil {
		return ""
	}
	var lines []string
	for _, c := range cg.List {
		lines = append(lines, c.Text)
	}
	return strings.Join(lines, "\n")
}

// getMethodDocComment extracts the doc comment for a method.
func getMethodDocComment(field *ast.Field) (docText, rawText string) {
	if field.Doc == nil {
		return "", ""
	}
	return field.Doc.Text(), commentGroupRaw(field.Doc)
}

// parseHostServiceAnnotation extracts //nd:hostservice annotation parameters.
func parseHostServiceAnnotation(doc string) map[string]string {
	for _, line := range strings.Split(doc, "\n") {
		line = strings.TrimSpace(line)
		match := hostServicePattern.FindStringSubmatch(line)
		if match != nil {
			return parseKeyValuePairs(match[1])
		}
	}
	return nil
}

// parseHostFuncAnnotation extracts //nd:hostfunc annotation parameters.
func parseHostFuncAnnotation(doc string) map[string]string {
	for _, line := range strings.Split(doc, "\n") {
		line = strings.TrimSpace(line)
		match := hostFuncPattern.FindStringSubmatch(line)
		if match != nil {
			params := parseKeyValuePairs(match[1])
			if params == nil {
				params = make(map[string]string)
			}
			return params
		}
	}
	return nil
}

// parseCapabilityAnnotation extracts //nd:capability annotation parameters.
func parseCapabilityAnnotation(doc string) map[string]string {
	for _, line := range strings.Split(doc, "\n") {
		line = strings.TrimSpace(line)
		match := capabilityPattern.FindStringSubmatch(line)
		if match != nil {
			return parseKeyValuePairs(match[1])
		}
	}
	return nil
}

// parseExportAnnotation extracts //nd:export annotation parameters.
func parseExportAnnotation(doc string) map[string]string {
	for _, line := range strings.Split(doc, "\n") {
		line = strings.TrimSpace(line)
		match := exportPattern.FindStringSubmatch(line)
		if match != nil {
			return parseKeyValuePairs(match[1])
		}
	}
	return nil
}

// parseKeyValuePairs extracts key=value pairs from annotation text.
func parseKeyValuePairs(text string) map[string]string {
	matches := keyValuePattern.FindAllStringSubmatch(text, -1)
	if len(matches) == 0 {
		return nil
	}
	result := make(map[string]string)
	for _, m := range matches {
		result[m[1]] = m[2]
	}
	return result
}

// parseMethod parses a method signature into a Method struct.
func parseMethod(name string, funcType *ast.FuncType, annotation map[string]string, doc string) (Method, error) {
	m := Method{
		Name:       name,
		ExportName: annotation["name"],
		Doc:        doc,
	}

	// Parse parameters (skip context.Context)
	if funcType.Params != nil {
		for _, field := range funcType.Params.List {
			typeName := typeToString(field.Type)
			if typeName == "context.Context" {
				continue // Skip context parameter
			}

			for _, name := range field.Names {
				m.Params = append(m.Params, NewParam(name.Name, typeName))
			}
		}
	}

	// Parse return values
	if funcType.Results != nil {
		for _, field := range funcType.Results.List {
			typeName := typeToString(field.Type)
			if typeName == "error" {
				m.HasError = true
				continue // Track error but don't include in Returns
			}

			// Handle anonymous returns
			if len(field.Names) == 0 {
				// Generate a name based on position
				m.Returns = append(m.Returns, NewParam("result", typeName))
			} else {
				for _, name := range field.Names {
					m.Returns = append(m.Returns, NewParam(name.Name, typeName))
				}
			}
		}
	}

	return m, nil
}

// typeToString converts an AST type expression to a string.
func typeToString(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.SelectorExpr:
		return typeToString(t.X) + "." + t.Sel.Name
	case *ast.StarExpr:
		return "*" + typeToString(t.X)
	case *ast.ArrayType:
		if t.Len == nil {
			return "[]" + typeToString(t.Elt)
		}
		return fmt.Sprintf("[%s]%s", typeToString(t.Len), typeToString(t.Elt))
	case *ast.MapType:
		return fmt.Sprintf("map[%s]%s", typeToString(t.Key), typeToString(t.Value))
	case *ast.BasicLit:
		return t.Value
	case *ast.InterfaceType:
		// Empty interface (interface{} or any)
		if t.Methods == nil || len(t.Methods.List) == 0 {
			return "any"
		}
		// Non-empty interfaces can't be easily represented
		return "any"
	default:
		return fmt.Sprintf("%T", expr)
	}
}

// cleanDoc removes annotation lines from documentation.
func cleanDoc(doc string) string {
	var lines []string
	for _, line := range strings.Split(doc, "\n") {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "//nd:") {
			continue
		}
		lines = append(lines, line)
	}
	return strings.TrimSpace(strings.Join(lines, "\n"))
}