// Copyright 2024 CUE Authors
//
// 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 gotypes

import (
	"bytes"
	"fmt"
	goast "go/ast"
	goformat "go/format"
	goparser "go/parser"
	goscanner "go/scanner"
	gotoken "go/token"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	goastutil "golang.org/x/tools/go/ast/astutil"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/build"
)

// Generate produces Go type definitions from exported CUE definitions.
// See the help text for `cue help exp gengotypes`.
func Generate(ctx *cue.Context, insts ...*build.Instance) error {
	// record which package instances have already been generated
	instDone := make(map[*build.Instance]bool)

	goPkgNamesDoneByDir := make(map[string]string)

	g := generator{generatedTypes: make(map[qualifiedPath]*generatedDef)}

	// ensure we don't modify the parameter slice
	insts = slices.Clip(insts)
	for len(insts) > 0 { // we append imports to this list
		inst := insts[0]
		insts = insts[1:]
		if err := inst.Err; err != nil {
			return err
		}
		if instDone[inst] {
			continue
		}
		instDone[inst] = true

		instVal := ctx.BuildInstance(inst)
		if err := instVal.Validate(); err != nil {
			return err
		}
		g.pkg = inst
		g.emitDefs = nil
		g.pkgRoot = instVal
		g.importCuePkgAsGoPkg = make(map[string]string)

		// gather the import aliases for the instance.
		// NOTE: this implementation means that if an import alias is employed in one file
		// it will be used anywhere that package is referenced
		importAliases, err := gatherImportAliases(inst)
		if err != nil {
			return err
		}
		g.importAliases = importAliases

		iter, err := instVal.Fields(cue.Definitions(true))
		if err != nil {
			return err
		}
		// TODO: support ignoring an entire package via a @go(-) package attribute.
		// TODO: support ignoring an entire file via a @go(-) file attribute above a package clause.
		for iter.Next() {
			sel := iter.Selector()
			if !sel.IsDefinition() {
				continue
			}
			path := cue.MakePath(sel)
			if _, err := g.genDef(path, iter.Value()); err != nil {
				return err
			}
		}

		// TODO: we should refuse to generate for packages which are not
		// part of the main module, as they may be inside the read-only module cache.
		for _, imp := range inst.Imports {
			if !instDone[imp] && g.importCuePkgAsGoPkg[imp.ImportPath] != "" {
				insts = append(insts, imp)
			}
		}

		var buf []byte
		printf := func(format string, args ...any) {
			buf = fmt.Appendf(buf, format, args...)
		}
		printf("// Code generated by \"cue exp gengotypes\"; DO NOT EDIT.\n\n")
		goPkgName := goPkgNameForInstance(inst, instVal, nil)
		if prev, ok := goPkgNamesDoneByDir[inst.Dir]; ok && prev != goPkgName {
			return fmt.Errorf("cannot generate two Go packages in one directory; %s and %s", prev, goPkgName)
		} else {
			goPkgNamesDoneByDir[inst.Dir] = goPkgName
		}
		printf("package %s\n\n", goPkgName)
		importedGo := slices.Sorted(maps.Values(g.importCuePkgAsGoPkg))
		importedGo = slices.Compact(importedGo)
		if len(importedGo) > 0 {
			printf("import (\n")
			for _, path := range importedGo {
				if alias, ok := g.importAliases[path]; ok {
					printf("\t%s %q\n", alias, path)
				} else {
					printf("\t%q\n", path)
				}
			}
			printf(")\n")
		}
		for _, path := range g.emitDefs {
			qpath := g.qualifiedPath(path)

			val := instVal.LookupPath(path)
			goName := goNameFromPath(path, true)
			if goName == "" {
				return fmt.Errorf("unexpected path in emitDefs: %q", qpath)
			}
			goAttr := goValueAttr(val)
			if s, _ := goAttr.String(0); s != "" {
				if s == "-" {
					continue
				}
				goName = s
			}

			emitDocs(printf, goName, val.Doc())
			printf("type %s ", goName)

			// As we grab the generated source, do some sanity checks too.
			gen, ok := g.generatedTypes[qpath]
			if !ok {
				return fmt.Errorf("expected type in generatedTypes: %q", qpath)
			}
			if gen.inProgress {
				return fmt.Errorf("unexpected in-progress type in generatedTypes: %q", qpath)
			}
			if len(gen.src) == 0 {
				return fmt.Errorf("unexpected empty type in generatedTypes: %q", qpath)
			}
			buf = append(buf, gen.src...)
			printf("\n\n")
		}

		// The generated file is named after the CUE package, not the generated Go package,
		// as we can have multiple CUE packages in one directory all generating to one Go package.
		// To keep the filename short for common cases, if we are generating a CUE package
		// whose package name is implied from its import path, omit the package name element.
		basename := "cue_types_gen.go"
		ip := ast.ParseImportPath(inst.ImportPath)
		ip1 := ip
		ip1.Qualifier = ""
		ip1.ExplicitQualifier = false
		ip1 = ast.ParseImportPath(ip1.String())
		if ip.Qualifier != ip1.Qualifier {
			basename = fmt.Sprintf("cue_types_%s_gen.go", inst.PkgName)
		}
		outpath := filepath.Join(inst.Dir, basename)

		formatted, err := goformat.Source(buf)
		if err != nil {
			// Showing the generated Go code helps debug where the syntax error is.
			// This should only occur if our code generator is buggy.
			lines := bytes.Split(buf, []byte("\n"))
			var withLineNums []byte
			for i, line := range lines {
				withLineNums = fmt.Appendf(withLineNums, "% 4d: %s\n", i+1, line)
			}
			fmt.Fprintf(os.Stderr, "-- %s --\n%s\n--\n", filepath.ToSlash(outpath), withLineNums)
			return err
		}
		if err := os.WriteFile(outpath, formatted, 0o666); err != nil {
			return err
		}
	}
	return nil
}

// generator holds the state for generating Go code for one CUE package instance.
type generator struct {
	// Fields for the entire invocation, to track information about referenced definitions.

	// generatedTypes records CUE definitions which we have analyzed and translated
	// to Go type expressions.
	//
	// Analyzing types before we start emitting is useful so that, for instance,
	// a Go field can skip using a pointer to a Go type if the type is already nilable.
	generatedTypes map[qualifiedPath]*generatedDef

	// Fields for each package instance.

	pkg *build.Instance

	// emitDefs records paths for the definitions we should emit as Go types.
	emitDefs []cue.Path

	// importCuePkgAsGoPkg records which CUE packages need to be imported as which Go packages in the generated Go package.
	// This is collected as we emit types, given that some CUE fields and types are omitted
	// and we don't want to end up with unused Go imports.
	//
	// The keys are full CUE import paths; the values are their resulting Go import paths.
	importCuePkgAsGoPkg map[string]string

	// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value
	// belongs to the current package or not.
	pkgRoot cue.Value

	// importAliases maps package names to a given alias. In the case that there are multiple aliases present for
	// same package then we will use the first alias encountered. When the same alias is used for multiple packages
	// across different files, then a number will be added to the end of the alias to avoid conflicts.
	importAliases map[string]string

	// Fields for each definition.

	// def tracks the generation state for a single CUE definition.
	def *generatedDef
}

type qualifiedPath = string // [build.Instance.ImportPath] + " " + [cue.Path.String]

func (g *generator) qualifiedPath(path cue.Path) qualifiedPath {
	return g.pkg.ImportPath + " " + path.String()
}

// generatedDef holds information about a Go type generated for a CUE definition.
type generatedDef struct {
	// inProgress helps detect cyclic definitions and prevents emitting any Go source
	// before we are done analyzing and generating the relevant types.
	inProgress bool

	// facts records useful information about the generated type.
	// Note that this only records the facts about the top-level type generated for a definition;
	// the facts about its sub-types, such as the types of fields in a struct,
	// are computed by recursive calls to [generator.emitType] but are not recorded here.
	facts typeFacts

	// src is the generated Go type expression source.
	// We generate types as plaintext Go source rather than [goast.Expr]
	// as the latter makes it very hard to use empty lines and comment placement correctly.
	src []byte
}

// typeFacts holds useful information about a generated type,
// such as how it was configured by the user, or qualities about the generated Go type.
type typeFacts struct {
	// isTypeOverride records whether the generated type came from a @go(,type=expr) expression.
	isTypeOverride bool

	// isNillable records whether the generated Go type can be compared to nil,
	// such that @go(,optional=nillable) can avoid wrapping it in a pointer.
	isNillable bool
}

func (g *generatedDef) printf(format string, args ...any) {
	if !g.inProgress {
		// It only makes sense to append to src while we are building the Go type expression.
		// If we append bytes after we're done, it's pointless, and likely a bug.
		panic("generatedDef.printf called when inProgress is false")
	}
	g.src = fmt.Appendf(g.src, format, args...)
}

type optionalStrategy int

const (
	_ optionalStrategy = iota
	// optional=zero (default); emit the Go type as-is and rely on the zero value.
	optionalZero
	// optional=nillable; emit the Go type with a pointer unless it can already
	// be compared to nil.
	optionalNillable
)

// genDef analyzes and generates a CUE definition as a Go type,
// adding it to [generator.generatedTypes] as well as [generator.emitDefs]
// to ensure that it is emitted as part of the resulting Go source.
func (g *generator) genDef(path cue.Path, val cue.Value) (*generatedDef, error) {
	qpath := g.qualifiedPath(path)
	if def, ok := g.generatedTypes[qpath]; ok {
		return def, nil // already done or in progress
	}
	g.emitDefs = append(g.emitDefs, path)

	// When generating a Go type for a CUE definition, we may recurse into
	// this very method if a CUE field references another definition.
	// Store the current [generatedDef] in the stack so we don't lose
	// what we have generated so far, while we generate the nested type.
	parentDef := g.def
	def := &generatedDef{inProgress: true}
	g.def = def
	g.generatedTypes[qpath] = def
	facts, err := g.emitType(val, optionalZero)
	if err != nil {
		return nil, err
	}
	g.def.facts = facts
	g.def.inProgress = false
	g.def = parentDef
	return def, nil
}

// emitType generates a CUE value as a Go type.
// When possible, the Go type is emitted in the form of a reference.
// Otherwise, an inline Go type expression is used.
func (g *generator) emitType(val cue.Value, optionalStg optionalStrategy) (typeFacts, error) {
	var facts typeFacts
	goAttr := goValueAttr(val)
	// We prefer the form @go(Name,type=pkg.Baz) as it is explicit and extensible,
	// but we are also backwards compatible with @go(Name,pkg.Baz) as emitted by `cue get go`.
	// Make sure that we don't mistake @go(,foo=bar) for a type though.
	attrType, _, _ := goAttr.Lookup(1, "type")
	if attrType == "" {
		if s, _ := goAttr.String(1); !strings.Contains(s, "=") {
			attrType = s
		}
	}
	if attrType != "" {
		fset := gotoken.NewFileSet()
		expr, importedByName, err := parseTypeExpr(fset, attrType)
		if err != nil {
			return facts, fmt.Errorf("cannot parse @go type expression: %w", err)
		}
		for _, pkgPath := range importedByName {
			g.importCuePkgAsGoPkg[pkgPath] = pkgPath
		}
		// Collect any remaining imports from selectors on unquoted single-element std packages
		// such as `@go(,type=io.Reader)`.
		expr = goastutil.Apply(expr, func(c *goastutil.Cursor) bool {
			if sel, _ := c.Node().(*goast.SelectorExpr); sel != nil {
				if imp, _ := sel.X.(*goast.Ident); imp != nil {
					if importedByName[imp.Name] != "" {
						// `@go(,type="go/constant".Kind)` ends up being parsed as the Go expression `constant.Kind`;
						// via importedByName we can tell that "constant" is already provided via "go/constant".
						return true
					}
					g.importCuePkgAsGoPkg[imp.Name] = imp.Name
				}
			}
			return true
		}, nil).(goast.Expr)
		var buf bytes.Buffer
		// We emit in plaintext, so format the parsed Go expression and print it out.
		if err := goformat.Node(&buf, fset, expr); err != nil {
			return facts, err
		}
		// TODO: try using go/packages or go/types to resolve this Go type
		// and find details about it, such as for [typeInfo.isNillable].
		g.def.printf("%s", buf.Bytes())
		facts.isTypeOverride = true
		return facts, nil
	}

	// Note that type references don't get optionalStg,
	// as @go(,optional=) only affects fields under the current type expression.
	// TODO: support nullable types, such as `null | #SomeReference` and `null | {foo: int}`.
	if done, facts, err := g.emitTypeReference(val); err != nil {
		return typeFacts{}, err
	} else if done {
		return facts, nil
	}

	// Inline types are below.

	switch k := val.IncompleteKind(); k {
	case cue.StructKind:
		if elem := val.LookupPath(cue.MakePath(cue.AnyString)); elem.Err() == nil {
			facts.isNillable = true // maps can be nil
			g.def.printf("map[string]")
			if _, err := g.emitType(elem, optionalStg); err != nil {
				return facts, err
			}
			break
		}
		// A disjunction of structs cannot be represented in Go, as it does not have sum types.
		// Fall back to a map of string to any, which is not ideal, but will work for any field.
		//
		// TODO: consider alternatives, such as:
		// * For `#StructFoo | #StructBar`, generate named types for each disjunct,
		//   and use `any` here as a sum type between them.
		// * For a disjunction of closed structs, generate a flat struct with the superset
		//   of all fields, akin to a C union.
		if op, _ := val.Expr(); op == cue.OrOp {
			facts.isNillable = true // maps can be nil
			g.def.printf("map[string]any")
			break
		}
		// TODO: treat a single embedding like `{[string]: int}` like we would `[string]: int`
		g.def.printf("struct {\n")
		iter, err := val.Fields(cue.Definitions(true), cue.Optional(true))
		if err != nil {
			return facts, err
		}
		for iter.Next() {
			sel := iter.Selector()
			val := iter.Value()
			if sel.IsDefinition() {
				// TODO: why does removing [cue.Definitions] above break the tests?
				continue
			}
			cueName := sel.String()
			if sel.IsString() {
				cueName = sel.Unquoted()
			}
			cueName = strings.TrimRight(cueName, "?!")
			emitDocs(g.def.printf, cueName, val.Doc())

			// We want the Go name from just this selector, even when it's not a definition.
			goName := goNameFromPath(cue.MakePath(sel), false)

			goAttr := val.Attribute("go")
			if s, _ := goAttr.String(0); s != "" {
				if s == "-" {
					continue
				}
				goName = s
			}

			optional := sel.ConstraintType()&cue.OptionalConstraint != 0
			optionalStg := optionalStg // only for this field

			// TODO: much like @go(-), support @(,optional=) when embedded in a value,
			// or attached to an entire package or file, to set a default for an entire scope.
			switch s, ok, _ := goAttr.Lookup(1, "optional"); s {
			case "zero":
				optionalStg = optionalZero
			case "nillable":
				optionalStg = optionalNillable
			default:
				if ok {
					return facts, fmt.Errorf("unknown optional strategy %q", s)
				}
			}

			// Since CUE fields using double quotes or commas in their names are rare,
			// and the upcoming encoding/json/v2 will support field tags with name quoting,
			// we choose to ignore such fields with a clear note for now.
			if strings.ContainsAny(cueName, "\\\"`,\n") {
				g.def.printf("// CUE field %q: encoding/json does not support this field name\n\n", cueName)
				continue
			}
			g.def.printf("%s ", goName)

			// Pointers in Go are a prefix in the syntax, but we won't find out the generated type facts
			// until we have emitted its Go source, which we do into the same buffer to avoid copies.
			// Luckily, since a pointer is always one byte, and we gofmt the result anyway for nice formatting,
			// we can add the pointer first and replace it with whitespace later if not wanted.
			ptrOffset := len(g.def.src)
			g.def.printf("*")

			facts, err := g.emitType(val, optionalStg)
			if err != nil {
				return facts, err
			}
			if !usePointer(facts, optional, optionalStg) {
				g.def.src[ptrOffset] = ' '
			}

			// TODO: should we generate cuego tags like `cue:"expr"`?
			// If not, at least move the /* CUE */ comments to the end of the line.
			omitEmpty := ""
			if optional {
				omitEmpty = ",omitempty"
			}
			g.def.printf(" `json:\"%s%s\"`", cueName, omitEmpty)
			g.def.printf("\n\n")
		}
		g.def.printf("}")
	case cue.ListKind:
		// We mainly care about patterns like [...string].
		// Anything else can convert into []any as a fallback.
		facts.isNillable = true // slices can be nil
		g.def.printf("[]")
		elem := val.LookupPath(cue.MakePath(cue.AnyIndex))
		if !elem.Exists() {
			// TODO: perhaps mention the original type.
			g.def.printf("any /* CUE closed list */")
		} else if _, err := g.emitType(elem, optionalStg); err != nil {
			return facts, err
		}

	case cue.NullKind:
		facts.isNillable = true // pointers can be nil
		g.def.printf("*struct{} /* CUE null */")
	case cue.BoolKind:
		g.def.printf("bool")
	case cue.IntKind:
		g.def.printf("int64")
	case cue.FloatKind:
		g.def.printf("float64")
	case cue.StringKind:
		g.def.printf("string")
	case cue.BytesKind:
		facts.isNillable = true // slices can be nil
		g.def.printf("[]byte")

	case cue.NumberKind:
		// Can we do better for numbers?
		facts.isNillable = true // interfaces can be nil
		g.def.printf("any /* CUE number; int64 or float64 */")

	case cue.TopKind:
		facts.isNillable = true // interfaces can be nil
		g.def.printf("any /* CUE top */")

	// TODO: generate e.g. int8 where appropriate
	// TODO: uint64 would be marginally better than int64 for unsigned integer types

	default:
		// A disjunction of various kinds cannot be represented in Go, as it does not have sum types.
		// Also see the potential approaches in the TODO about disjunctions of structs.
		if op, _ := val.Expr(); op == cue.OrOp {
			facts.isNillable = true // interfaces can be nil
			g.def.printf("any /* CUE disjunction: %s */", k)
			break
		}
		facts.isNillable = true // interfaces can be nil
		g.def.printf("any /* TODO: IncompleteKind: %s */", k)
	}
	return facts, nil
}

func usePointer(facts typeFacts, optional bool, strategy optionalStrategy) bool {
	if facts.isTypeOverride {
		// @(,type=) overrides any @(,optional=) setting
		return false
	}
	if !optional {
		// Regular and required fields never use pointers.
		return false
	}
	switch strategy {
	case optionalZero:
		return false
	case optionalNillable:
		// Only use a pointer when the type isn't already nillable.
		return !facts.isNillable
	default:
		panic("unreachable")
	}
}

// parseTypeExpr extends [goparser.ParseExpr] to allow selecting from full import paths.
// `[]go/constant.Kind` is not a valid Go expression, and `[]constant.Kind` is valid
// but doesn't specify a full import path, so it's ambiguous.
//
// Accept `[]"go/constant".Kind` with a pre-processing step to find quoted strings,
// record them as imports keyed by package name in the returned map,
// and rewrite the Go expression to be in terms of the imported package.
// Note that a pre-processing step is necessary as ParseExpr rejects this custom syntax.
func parseTypeExpr(fset *gotoken.FileSet, src string) (goast.Expr, map[string]string, error) {
	var goSrc strings.Builder
	importedByName := make(map[string]string)

	var scan goscanner.Scanner
	scan.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), nil, 0)
	lastStringLit := ""
	for {
		_, tok, lit := scan.Scan()
		if tok == gotoken.EOF {
			break
		}
		if lastStringLit != "" {
			if tok == gotoken.PERIOD {
				imp, err := strconv.Unquote(lastStringLit)
				if err != nil {
					panic(err) // should never happen
				}
				// We assume the package name is the last path component.
				// TODO: consider how we might support renaming imports,
				// so that importing both foo.com/x and bar.com/x is possible.
				_, impName, _ := cutLast(imp, "/")
				importedByName[impName] = imp
				goSrc.WriteString(impName)
			} else {
				goSrc.WriteString(lastStringLit)
			}
			lastStringLit = ""
		}
		switch tok {
		case gotoken.STRING:
			lastStringLit = lit
		case gotoken.IDENT, gotoken.INT, gotoken.FLOAT, gotoken.IMAG, gotoken.CHAR:
			goSrc.WriteString(lit)
		case gotoken.SEMICOLON:
			// TODO: How can we support multi-line types such as structs?
			// Note that EOF inserts a semicolon, which breaks goparser.ParseExpr.
			if lit == "\n" {
				break // inserted semicolon at EOF
			}
			fallthrough
		default:
			goSrc.WriteString(tok.String())
		}
	}
	expr, err := goparser.ParseExpr(goSrc.String())
	return expr, importedByName, err
}

func cutLast(s, sep string) (before, after string, found bool) {
	if i := strings.LastIndex(s, sep); i >= 0 {
		return s[:i], s[i+len(sep):], true
	}
	return "", s, false
}

// goNameFromPath transforms a CUE path, such as "#foo.bar?",
// into a suitable name for a generated Go type, such as "Foo_bar".
// When defsOnly is true, all path elements must be definitions, or "" is returned.
func goNameFromPath(path cue.Path, defsOnly bool) string {
	export := true
	var sb strings.Builder
	for i, sel := range path.Selectors() {
		if defsOnly && !sel.IsDefinition() {
			return ""
		}
		if i > 0 {
			// To aid in readability, nested names are separated with underscores.
			sb.WriteString("_")
		}
		str := sel.String()
		if sel.IsString() {
			str = sel.Unquoted()
		}
		str, hidden := strings.CutPrefix(str, "_")
		if hidden {
			// If any part of the path is hidden, we are not exporting.
			export = false
		}
		// Leading or trailing characters for definitions, optional, or required
		// are not included as part of Go names.
		str = strings.TrimPrefix(str, "#")
		str = strings.TrimRight(str, "?!")
		// CUE allows quoted field names such as "foo-bar" or "123baz",
		// none of which are valid Go identifiers per https://go.dev/ref/spec#Identifiers.
		// Replace forbidden characters with underscores, like `go test` does with subtest names,
		// and add a leading "F" if the name begins with a digit.
		// TODO: this could result in name collisions; fix if it actually happens in practice.
		for i, r := range str {
			switch {
			case unicode.IsLetter(r):
				sb.WriteRune(r)
			case unicode.IsDigit(r):
				if i == 0 {
					sb.WriteRune('F')
				}
				sb.WriteRune(r)
			default:
				sb.WriteRune('_')
			}
		}
	}
	name := sb.String()
	if export {
		// Capitalize the first letter to export the name in Go.
		// https://go.dev/ref/spec#Exported_identifiers
		first, size := utf8.DecodeRuneInString(name)
		name = string(unicode.ToTitle(first)) + name[size:]
	}
	// TODO: lowercase if not exporting
	return name
}

// goValueAttr is like [cue.Value.Attribute] with the string parameter "go",
// but it supports [cue.DeclAttr] attributes as well and not just [cue.FieldAttr].
//
// TODO: surely this is a shortcoming of the method above?
func goValueAttr(val cue.Value) cue.Attribute {
	attrs := val.Attributes(cue.ValueAttr)
	for _, attr := range attrs {
		if attr.Name() == "go" {
			return attr
		}
	}
	return cue.Attribute{}
}

// goPkgNameForInstance determines what to name a Go package generated from a CUE instance.
// By default this is the CUE package name, but it can be overriden by a @go() package attribute.
// When supplying importAliases, and if no package attribute is found, the returned package name
// reflects the alias name that the package is being imported as.
func goPkgNameForInstance(inst *build.Instance, instVal cue.Value, importAliases map[string]string) string {
	attr := goValueAttr(instVal)
	if s, _ := attr.String(0); s != "" {
		return s
	}
	if alias, ok := importAliases[inst.ImportPath]; ok {
		return alias
	}
	return inst.PkgName
}

// emitTypeReference attempts to generate a CUE value as a Go type via a reference,
// either to a type in the same Go package, or to a type in an imported package.
func (g *generator) emitTypeReference(val cue.Value) (bool, typeFacts, error) {
	// References to existing names, either from the same package or an imported package.
	root, path := val.ReferencePath()
	// TODO: surely there is a better way to check whether ReferencePath returned "no path",
	// such as a possible path.IsValid method?
	if len(path.Selectors()) == 0 {
		return false, typeFacts{}, nil
	}
	inst := root.BuildInstance()
	// Go has no notion of qualified import paths; if a CUE file imports
	// "foo.com/bar:qualified", we import just "foo.com/bar" on the Go side.
	// TODO: deal with multiple packages existing in the same directory.
	unqualifiedPath := ast.ParseImportPath(inst.ImportPath).Unqualified().String()

	// As a special case, some CUE standard library types are allowed as references
	// even though they aren't definitions.
	defsOnly := true
	switch fmt.Sprintf("%s.%s", unqualifiedPath, path) {
	case "time.Duration":
		// Note that CUE represents durations as strings, but Go as int64.
		// TODO: can we do better here, such as a custom duration type?
		g.def.printf("string /* CUE time.Duration */")
		return true, typeFacts{}, nil
	case "time.Time":
		defsOnly = false
	}

	name := goNameFromPath(path, defsOnly)
	if name == "" {
		return false, typeFacts{}, nil // Not a path we are generating.
	}

	var facts typeFacts
	inProgress := false
	// We did use a reference; if the referenced name was from another package,
	// we need to ensure that package is imported.
	// Otherwise, we need to ensure that the referenced local definition is generated.
	// Either way, return the facts about the referenced type.
	if root != g.pkgRoot {
		g.importCuePkgAsGoPkg[inst.ImportPath] = unqualifiedPath
		// TODO: populate the facts here, which will require generating imported packages first.
	} else {
		def, err := g.genDef(path, cue.Dereference(val))
		if err != nil {
			return false, typeFacts{}, err
		}
		facts = def.facts
		inProgress = def.inProgress
	}
	// We generate types depth-first; if the type referenced here is still in progress,
	// it means that we are in a cyclic type, so be nillable to avoid a Go type of infinite size.
	// Note that sometimes we're in a complex type which is already nillable, such as:
	//
	//    #GraphNode: {edges?: [...#GraphNode]}
	//
	// So we could generate the Go field as `[]GraphNode` rather than `[]*GraphNode`,
	// given that Go slices are already nillable, but we currently do use a pointer.
	if inProgress && !facts.isNillable {
		g.def.printf("*")
		facts.isNillable = true // pointers can be nil
	}
	if root != g.pkgRoot {
		g.def.printf("%s.", goPkgNameForInstance(inst, root, g.importAliases))
	}
	g.def.printf("%s", name)
	return true, facts, nil
}

// emitDocs generates the documentation comments attached to the following declaration.
// It takes a printf function as we emit docs directly in the generated Go code
// when emitting the top-level Go type definitions.
func emitDocs(printf func(string, ...any), name string, groups []*ast.CommentGroup) {
	// TODO: place the comment group starting with `// $name ...` first.
	// TODO: ensure that the Go name is used in the godoc.
	for i, group := range groups {
		if i > 0 {
			printf("//\n")
		}
		for _, line := range group.List {
			printf("%s\n", line.Text)
		}
	}
}

// gatherImportAliases collects the aliases from imports across the instance.
func gatherImportAliases(inst *build.Instance) (map[string]string, error) {
	fileAliases := make(map[string]string)
	tracked := make(map[string]int)

	type pair struct{ path, alias string }
	var explicit []pair

	for _, file := range inst.Files {
		for spec := range file.ImportSpecs() {
			pkgPath, err := strconv.Unquote(spec.Path.Value)
			if err != nil {
				return nil, err
			}
			// Unaliased import: reserve its package name, which can come
			// from an explicit or implicit qualifier.
			if spec.Name == nil {
				pkgName := ast.ParseImportPath(pkgPath).Qualifier
				if pkgName == "" {
					return nil, fmt.Errorf("could not find an imported package qualifier: %q", pkgPath)
				}
				tracked[pkgName]++
				continue
			}
			// Explicit alias: queue for resolution.
			alias := spec.Name.Name
			explicit = append(explicit, pair{path: pkgPath, alias: alias})
		}
	}

	// Resolve explicit aliases with conflict suffixing.
	for _, e := range explicit {
		alias := e.alias
		if count, ok := tracked[alias]; ok {
			alias = fmt.Sprintf("%s%d", alias, count)
		}
		fileAliases[e.path] = alias
		tracked[e.alias]++ // track the alias name (unsuffixed) for future conflicts
	}

	return fileAliases, nil
}
