// Copyright 2019 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 openapi

import (
	"cmp"
	"fmt"
	"maps"
	"math"
	"path"
	"regexp"
	"slices"
	"strings"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal"
	"cuelang.org/go/internal/core/adt"
)

type buildContext struct {
	inst      cue.Value
	instExt   cue.Value
	refPrefix string
	path      []cue.Selector
	errs      errors.Error

	expandRefs    bool
	structural    bool
	exclusiveBool bool
	nameFunc      func(inst cue.Value, path cue.Path) string
	descFunc      func(v cue.Value) string
	fieldFilter   *regexp.Regexp

	schemas *orderedMap

	// Track external schemas.
	externalRefs map[string]*externalType

	// Used for cycle detection in case of using ExpandReferences. At the
	// moment, CUE does not detect cycles when a user forcefully steps into a
	// pattern constraint.
	//
	// TODO: consider an option in the CUE API where optional fields are
	// recursively evaluated.
	cycleNodes []*adt.Vertex
}

type externalType struct {
	ref   string
	inst  cue.Value
	path  cue.Path
	value cue.Value
}

type typeFunc func(b *builder, a cue.Value)

func schemas(g *Generator, inst cue.InstanceOrValue) (schemas *ast.StructLit, err error) {
	val := inst.Value()
	var fieldFilter *regexp.Regexp
	if g.FieldFilter != "" {
		fieldFilter, err = regexp.Compile(g.FieldFilter)
		if err != nil {
			return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err)
		}

		// verify that certain elements are still passed.
		for _, f := range strings.Split(
			"version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+
				"nullable,type", ",") {
			if fieldFilter.MatchString(f) {
				return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f)
			}
		}
	}

	if g.Version == "" {
		g.Version = "3.0.0"
	}

	c := &buildContext{
		inst:         val,
		instExt:      val,
		refPrefix:    "components/schemas",
		expandRefs:   g.ExpandReferences,
		structural:   g.ExpandReferences,
		nameFunc:     g.NameFunc,
		descFunc:     g.DescriptionFunc,
		schemas:      &orderedMap{},
		externalRefs: map[string]*externalType{},
		fieldFilter:  fieldFilter,
	}

	switch g.Version {
	case "3.0.0":
		c.exclusiveBool = true
	case "3.1.0":
	default:
		return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version)
	}

	defer func() {
		switch x := recover().(type) {
		case nil:
		case *openapiError:
			err = x
		default:
			panic(x)
		}
	}()

	// Although paths is empty for now, it makes it valid OpenAPI spec.

	i, err := inst.Value().Fields(cue.Definitions(true))
	if err != nil {
		return nil, err
	}
	for i.Next() {
		sel := i.Selector()
		if !sel.IsDefinition() {
			continue
		}
		// message, enum, or constant.
		if c.isInternal(sel) {
			continue
		}
		ref := c.makeRef(val, cue.MakePath(sel))
		if ref == "" {
			continue
		}
		c.schemas.setExpr(ref, c.build(sel, i.Value()))
	}

	// keep looping until a fixed point is reached.
	for done := 0; len(c.externalRefs) != done; {
		done = len(c.externalRefs)

		// From now on, all references need to be expanded
		for _, k := range slices.Sorted(maps.Keys(c.externalRefs)) {
			ext := c.externalRefs[k]
			c.instExt = ext.inst
			sels := ext.path.Selectors()
			last := len(sels) - 1
			c.path = sels[:last]
			name := sels[last]
			c.schemas.setExpr(ext.ref, c.build(name, cue.Dereference(ext.value)))
		}
	}

	slices.SortFunc(c.schemas.Elts, func(a, b ast.Decl) int {
		return cmp.Compare(label(a), label(b))
	})

	return (*ast.StructLit)(c.schemas), c.errs
}

func (c *buildContext) build(name cue.Selector, v cue.Value) *ast.StructLit {
	return newCoreBuilder(c).schema(nil, name, v)
}

// isInternal reports whether or not to include this type.
func (c *buildContext) isInternal(sel cue.Selector) bool {
	// TODO: allow a regexp filter in Config. If we have closed structs and
	// definitions, this will likely be unnecessary.
	return sel.Type().LabelType() == cue.DefinitionLabel &&
		strings.HasSuffix(sel.String(), "_value")
}

func (b *builder) failf(v cue.Value, format string, args ...interface{}) {
	panic(&openapiError{
		errors.NewMessagef(format, args...),
		cue.MakePath(b.ctx.path...),
		v.Pos(),
	})
}

func (b *builder) unsupported(v cue.Value) {
	if b.format == "" {
		// Not strictly an error, but consider listing it as a warning
		// in strict mode.
	}
}

func (b *builder) checkArgs(a []cue.Value, n int) {
	if len(a)-1 != n {
		b.failf(a[0], "%v must be used with %d arguments", a[0], len(a)-1)
	}
}

func (b *builder) schema(core *builder, name cue.Selector, v cue.Value) *ast.StructLit {
	oldPath := b.ctx.path
	b.ctx.path = append(b.ctx.path, name)
	defer func() { b.ctx.path = oldPath }()

	var c *builder
	if core == nil && b.ctx.structural {
		c = newCoreBuilder(b.ctx)
		c.buildCore(v) // initialize core structure
		c.coreSchema()
	} else {
		c = newRootBuilder(b.ctx)
		c.core = core
	}

	return c.fillSchema(v)
}

func (b *builder) getDoc(v cue.Value) {
	doc := []string{}
	if b.ctx.descFunc != nil {
		if str := b.ctx.descFunc(v); str != "" {
			doc = append(doc, str)
		}
	} else {
		for _, d := range v.Doc() {
			doc = append(doc, d.Text())
		}
	}
	if len(doc) > 0 {
		str := strings.TrimSpace(strings.Join(doc, "\n\n"))
		b.setSingle("description", ast.NewString(str), true)
	}
}

func (b *builder) fillSchema(v cue.Value) *ast.StructLit {
	if b.filled != nil {
		return b.filled
	}

	b.setValueType(v)
	b.format = extractFormat(v)
	b.deprecated = getDeprecated(v)

	if b.core == nil || len(b.core.values) > 1 {
		isRef := b.value(v, nil)
		if isRef {
			b.typ = ""
		}

		if !isRef && !b.ctx.structural {
			b.getDoc(v)
		}
	}

	schema := b.finish()
	s := schema

	simplify(b, s)

	sortSchema(s)

	b.filled = s
	return s
}

func label(d ast.Decl) string {
	f := d.(*ast.Field)
	s, _, _ := ast.LabelName(f.Label)
	return s
}

func value(d ast.Decl) ast.Expr {
	return d.(*ast.Field).Value
}

func sortSchema(s *ast.StructLit) {
	slices.SortFunc(s.Elts, func(a, b ast.Decl) int {
		aName := label(a)
		bName := label(b)
		aOrder := fieldOrder[aName]
		bOrder := fieldOrder[bName]
		return cmp.Or(-cmp.Compare(aOrder, bOrder), cmp.Compare(aName, bName))
	})
}

var fieldOrder = map[string]int{
	"description":      31,
	"type":             30,
	"format":           29,
	"required":         28,
	"properties":       27,
	"minProperties":    26,
	"maxProperties":    25,
	"minimum":          24,
	"exclusiveMinimum": 23,
	"maximum":          22,
	"exclusiveMaximum": 21,
	"minItems":         18,
	"maxItems":         17,
	"minLength":        16,
	"maxLength":        15,
	"items":            14,
	"enum":             13,
	"default":          12,
}

func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) {
	b.pushNode(v)
	defer b.popNode()

	count := 0
	disallowDefault := false
	var values cue.Value
	if b.ctx.expandRefs || b.format != "" {
		values = cue.Dereference(v)
		count = 1
	} else {
		dedup := map[string]bool{}
		hasNoRef := false
		accept := v
		conjuncts := appendSplit(nil, cue.AndOp, v)
		for _, v := range conjuncts {
			// This may be a reference to an enum. So we need to check references before
			// dissecting them.

			switch v1, path := v.ReferencePath(); {
			case len(path.Selectors()) > 0:
				ref := b.ctx.makeRef(v1, path)
				if ref == "" {
					v = cue.Dereference(v)
					break
				}
				if dedup[ref] {
					continue
				}
				dedup[ref] = true

				b.addRef(v, v1, path)
				disallowDefault = true
				continue
			}
			hasNoRef = true
			count++
			values = values.UnifyAccept(v, accept)
		}
		isRef = !hasNoRef && len(dedup) == 1
	}

	if count > 0 { // TODO: implement IsAny.
		// TODO: perhaps find optimal representation. For now we assume the
		// representation as is already optimized for human consumption.
		if values.IncompleteKind()&cue.StructKind != cue.StructKind && !isRef {
			values = values.Eval()
		}

		conjuncts := appendSplit(nil, cue.AndOp, values)
		for i, v := range conjuncts {
			switch {
			case isConcrete(v):
				b.dispatch(f, v)
				if !b.isNonCore() {
					b.set("enum", ast.NewList(b.decode(v)))
				}
			default:
				a := appendSplit(nil, cue.OrOp, v)
				for i, v := range a {
					if _, r := v.ReferencePath(); len(r.Selectors()) == 0 {
						a[i] = v.Eval()
					}
				}

				_ = i
				// TODO: it matters here whether a conjunct is obtained
				// from embedding or normal unification. Fix this at some
				// point.
				//
				// if len(a) > 1 {
				// 	// Filter disjuncts that cannot unify with other conjuncts,
				// 	// and thus can never be satisfied.
				// 	// TODO: there should be generalized simplification logic
				// 	// in CUE (outside of the usual implicit simplifications).
				// 	k := 0
				// outer:
				// 	for _, d := range a {
				// 		for j, w := range conjuncts {
				// 			if i == j {
				// 				continue
				// 			}
				// 			if d.Unify(w).Err() != nil {
				// 				continue outer
				// 			}
				// 		}
				// 		a[k] = d
				// 		k++
				// 	}
				// 	a = a[:k]
				// }
				switch len(a) {
				case 0:
					// Conjunct entirely eliminated.
				case 1:
					v = a[0]
					if err := v.Err(); err != nil {
						b.failf(v, "openapi: %v", err)
						return
					}
					b.dispatch(f, v)
				default:
					b.disjunction(a, f)
				}
			}
		}
	}

	if v, ok := v.Default(); ok && v.IsConcrete() && !disallowDefault {
		// TODO: should we show the empty list default? This would be correct
		// but perhaps a bit too pedantic and noisy.
		switch {
		case v.Kind() == cue.ListKind:
			iter, _ := v.List()
			if !iter.Next() {
				// Don't show default for empty list.
				break
			}
			fallthrough
		default:
			if !b.isNonCore() {
				e := v.Syntax(cue.Concrete(true)).(ast.Expr)
				b.setFilter("Schema", "default", e)
			}
		}
	}
	return isRef
}

func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value {
	op, args := v.Expr()
	// dedup elements.
	k := 1
outer:
	for i := 1; i < len(args); i++ {
		for j := 0; j < k; j++ {
			if args[i].Subsume(args[j], cue.Raw()) == nil &&
				args[j].Subsume(args[i], cue.Raw()) == nil {
				continue outer
			}
		}
		args[k] = args[i]
		k++
	}
	args = args[:k]

	if op == cue.NoOp && len(args) == 1 {
		// TODO: this is to deal with default value removal. This may change
		// when we completely separate default values from values.
		a = append(a, args...)
	} else if op != splitBy {
		a = append(a, v)
	} else {
		for _, v := range args {
			a = appendSplit(a, splitBy, v)
		}
	}
	return a
}

// isConcrete reports whether v is concrete and not a struct (recursively).
// structs are not supported as the result of a struct enum depends on how
// conjunctions and disjunctions are distributed. We could consider still doing
// this if we define a normal form.
func isConcrete(v cue.Value) bool {
	if !v.IsConcrete() {
		return false
	}
	if v.Kind() == cue.StructKind || v.Kind() == cue.ListKind {
		return false // TODO: handle struct and list kinds
	}
	return true
}

func (b *builder) disjunction(a []cue.Value, f typeFunc) {
	disjuncts := []cue.Value{}
	enums := []ast.Expr{} // TODO: unique the enums
	nullable := false     // Only supported in OpenAPI, not JSON schema

	for _, v := range a {
		switch {
		case v.Null() == nil:
			// TODO: for JSON schema, we need to fall through.
			nullable = true

		case isConcrete(v):
			enums = append(enums, b.decode(v))

		default:
			disjuncts = append(disjuncts, v)
		}
	}

	// Only one conjunct?
	if len(disjuncts) == 0 || (len(disjuncts) == 1 && len(enums) == 0) {
		if len(disjuncts) == 1 {
			b.value(disjuncts[0], f)
		}
		if len(enums) > 0 && !b.isNonCore() {
			b.set("enum", ast.NewList(enums...))
		}
		if nullable {
			b.setSingle("nullable", ast.NewBool(true), true) // allowed in Structural
		}
		return
	}

	anyOf := []ast.Expr{}
	if len(enums) > 0 {
		anyOf = append(anyOf, b.kv("enum", ast.NewList(enums...)))
	}

	if nullable {
		b.setSingle("nullable", ast.NewBool(true), true)
	}

	schemas := make([]*ast.StructLit, len(disjuncts))
	for i, v := range disjuncts {
		c := newOASBuilder(b)
		c.value(v, f)
		t := c.finish()
		schemas[i] = t
		if len(t.Elts) == 0 {
			if c.typ == "" {
				return
			}
		}
	}

	for i, v := range disjuncts {
		// In OpenAPI schema are open by default. To ensure forward compatibility,
		// we do not represent closed structs with additionalProperties: false
		// (this is discouraged and often disallowed by implementions), but
		// rather enforce this by ensuring uniqueness of the disjuncts.
		//
		// TODO: subsumption may currently give false negatives. We are extra
		// conservative in these instances.
		subsumed := []ast.Expr{}
		for j, w := range disjuncts {
			if i == j {
				continue
			}
			err := v.Subsume(w, cue.Schema())
			if err == nil || errors.Is(err, internal.ErrInexact) {
				subsumed = append(subsumed, schemas[j])
			}
		}

		t := schemas[i]
		if len(subsumed) > 0 {
			// TODO: elide anyOf if there is only one element. This should be
			// rare if originating from oneOf.
			exclude := ast.NewStruct("not",
				ast.NewStruct("anyOf", ast.NewList(subsumed...)))
			if len(t.Elts) == 0 {
				t = exclude
			} else {
				t = ast.NewStruct("allOf", ast.NewList(t, exclude))
			}
		}
		anyOf = append(anyOf, t)
	}

	b.set("oneOf", ast.NewList(anyOf...))
}

func (b *builder) setValueType(v cue.Value) {
	if b.core != nil {
		return
	}

	k := v.IncompleteKind() &^ adt.NullKind
	switch k {
	case cue.BoolKind:
		b.typ = "boolean"
	case cue.FloatKind, cue.NumberKind:
		b.typ = "number"
	case cue.IntKind:
		b.typ = "integer"
	case cue.BytesKind:
		b.typ = "string"
	case cue.StringKind:
		b.typ = "string"
	case cue.StructKind:
		b.typ = "object"
	case cue.ListKind:
		b.typ = "array"
	}
}

func (b *builder) dispatch(f typeFunc, v cue.Value) {
	if f != nil {
		f(b, v)
		return
	}

	switch v.IncompleteKind() {
	case cue.NullKind:
		// TODO: for JSON schema we would set the type here. For OpenAPI,
		// it must be nullable.
		b.setSingle("nullable", ast.NewBool(true), true)

	case cue.BoolKind:
		b.setType("boolean", "")
		// No need to call.

	case cue.FloatKind, cue.NumberKind:
		// TODO:
		// Common   Name	type	format	Comments
		// float	number	float
		// double	number	double
		b.setType("number", "") // may be overridden to integer
		b.number(v)

	case cue.IntKind:
		// integer	integer	int32	signed 	32 bits
		// long		integer	int64	signed 	64 bits
		b.setType("integer", "") // may be overridden to integer
		b.number(v)

		// TODO: for JSON schema, consider adding multipleOf: 1.

	case cue.BytesKind:
		// byte		string	byte	base64 	encoded characters
		// binary	string	binary	any 	sequence of octets
		b.setType("string", "byte")
		b.bytes(v)
	case cue.StringKind:
		// date		string			date	   As defined by full-date - RFC3339
		// dateTime	string			date-time  As defined by date-time - RFC3339
		// password	string			password   A hint to UIs to obscure input
		b.setType("string", "")
		b.string(v)
	case cue.StructKind:
		b.setType("object", "")
		b.object(v)
	case cue.ListKind:
		b.setType("array", "")
		b.array(v)
	}
}

// object supports the following
//   - maxProperties: maximum allowed fields in this struct.
//   - minProperties: minimum required fields in this struct.
//   - patternProperties: [regexp]: schema
//     TODO: we can support this once .kv(key, value) allow
//     foo [=~"pattern"]: type
//     An instance field must match all schemas for which a regexp matches.
//     Even though it is not supported in OpenAPI, we should still accept it
//     when receiving from OpenAPI. We could possibly use disjunctions to encode
//     this.
//   - dependencies: what?
//   - propertyNames: schema
//     every property name in the enclosed schema matches that of
func (b *builder) object(v cue.Value) {
	// TODO: discriminator objects: we could theoretically derive discriminator
	// objects automatically: for every object in a oneOf/allOf/anyOf, or any
	// object composed of the same type, if a property is required and set to a
	// constant value for each type, it is a discriminator.

	switch op, a := v.Expr(); op {
	case cue.CallOp:
		name := fmt.Sprint(a[0])
		switch name {
		case "struct.MinFields":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "minProperties", b.int(a[1]))
			return

		case "struct.MaxFields":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "maxProperties", b.int(a[1]))
			return

		default:
			b.unsupported(a[0])
			return
		}

	case cue.NoOp:
		// TODO: extract format from specific type.

	default:
		b.failf(v, "unsupported op %v for object type (%v)", op, v)
		return
	}

	required := []ast.Expr{}
	for i, _ := v.Fields(); i.Next(); {
		required = append(required, ast.NewString(i.Selector().Unquoted()))
	}
	if len(required) > 0 {
		b.setFilter("Schema", "required", ast.NewList(required...))
	}

	var properties *orderedMap
	if b.singleFields != nil {
		properties = b.singleFields.getMap("properties")
	}
	hasProps := properties != nil
	if !hasProps {
		properties = &orderedMap{}
	}

	for i, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); i.Next(); {
		sel := i.Selector()
		if b.ctx.isInternal(sel) {
			continue
		}
		label := selectorLabel(sel)
		var core *builder
		if b.core != nil {
			core = b.core.properties[label]
		}
		schema := b.schema(core, sel, i.Value())
		switch {
		case sel.IsDefinition():
			ref := b.ctx.makeRef(b.ctx.instExt, cue.MakePath(append(b.ctx.path, sel)...))
			if ref == "" {
				continue
			}
			b.ctx.schemas.setExpr(ref, schema)
		case !b.isNonCore() || len(schema.Elts) > 0:
			properties.setExpr(label, schema)
		}
	}

	if !hasProps && properties.len() > 0 {
		b.setSingle("properties", (*ast.StructLit)(properties), false)
	}

	if t := v.LookupPath(cue.MakePath(cue.AnyString)); t.Exists() &&
		(b.core == nil || b.core.items == nil) && b.checkCycle(t) {
		schema := b.schema(nil, cue.AnyString, t)
		if len(schema.Elts) > 0 {
			b.setSingle("additionalProperties", schema, true) // Not allowed in structural.
		}
	}

	// TODO: maxProperties, minProperties: can be done once we allow cap to
	// unify with structs.
}

// List constraints:
//
// Max and min items.
//   - maxItems: int (inclusive)
//   - minItems: int (inclusive)
//   - items (item type)
//     schema: applies to all items
//     array of schemas:
//     schema at pos must match if both value and items are defined.
//   - additional items:
//     schema: where items must be an array of schemas, intstance elements
//     succeed for if they match this value for any value at a position
//     greater than that covered by items.
//   - uniqueItems: bool
//     TODO: support with list.Unique() unique() or comprehensions.
//     For the latter, we need equality for all values, which is doable,
//     but not done yet.
//
// NOT SUPPORTED IN OpenAPI:
//   - contains:
//     schema: an array instance is valid if at least one element matches
//     this schema.
func (b *builder) array(v cue.Value) {

	switch op, a := v.Expr(); op {
	case cue.CallOp:
		name := fmt.Sprint(a[0])
		switch name {
		case "list.UniqueItems", "list.UniqueItems()":
			b.checkArgs(a, 0)
			b.setFilter("Schema", "uniqueItems", ast.NewBool(true))
			return

		case "list.MinItems":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "minItems", b.int(a[1]))
			return

		case "list.MaxItems":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "maxItems", b.int(a[1]))
			return

		default:
			b.unsupported(a[0])
			return
		}

	case cue.NoOp:
		// TODO: extract format from specific type.

	default:
		b.failf(v, "unsupported op %v for array type", op)
		return
	}

	// Possible conjuncts:
	//   - one list (CUE guarantees merging all conjuncts)
	//   - no cap: is unified with list
	//   - unique items: at most one, but idempotent if multiple.
	// There is never a need for allOf or anyOf. Note that a CUE list
	// corresponds almost one-to-one to OpenAPI lists.
	items := []ast.Expr{}
	count := 0
	for i, _ := v.List(); i.Next(); count++ {
		items = append(items, b.schema(nil, cue.Index(count), i.Value()))
	}
	if len(items) > 0 {
		// TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema.
		// Perhaps we should turn this into an OR after first normalizing
		// the entries.
		b.set("items", ast.NewList(items...))
		// panic("per-item types not supported in OpenAPI")
	}

	// TODO:
	// A CUE cap can be a set of discontinuous ranges. If we encounter this,
	// we can create an allOf(list type, anyOf(ranges)).
	cap := v.Len()
	hasMax := false
	maxLength := int64(math.MaxInt64)

	if n, capErr := cap.Int64(); capErr == nil {
		maxLength = n
		hasMax = true
	} else {
		b.value(cap, (*builder).listCap)
	}

	if !hasMax || int64(len(items)) < maxLength {
		if typ := v.LookupPath(cue.MakePath(cue.AnyIndex)); typ.Exists() && b.checkCycle(typ) {
			var core *builder
			if b.core != nil {
				core = b.core.items
			}
			t := b.schema(core, cue.AnyString, typ)
			if len(items) > 0 {
				b.setFilter("Schema", "additionalItems", t) // Not allowed in structural.
			} else if !b.isNonCore() || len(t.Elts) > 0 {
				b.setSingle("items", t, true)
			}
		}
	}
}

func (b *builder) listCap(v cue.Value) {
	switch op, a := v.Expr(); op {
	case cue.LessThanOp:
		b.setFilter("Schema", "maxItems", b.inta(a[0], -1))
	case cue.LessThanEqualOp:
		b.setFilter("Schema", "maxItems", b.inta(a[0], 0))
	case cue.GreaterThanOp:
		b.setFilter("Schema", "minItems", b.inta(a[0], 1))
	case cue.GreaterThanEqualOp:
		if b.int64(a[0]) > 0 {
			b.setFilter("Schema", "minItems", b.inta(a[0], 0))
		}
	case cue.NoOp:
		// must be type, so okay.
	case cue.NotEqualOp:
		i := b.int(a[0])
		b.setNot("allOff", ast.NewList(
			b.kv("minItems", i),
			b.kv("maxItems", i),
		))

	default:
		b.failf(v, "unsupported op for list capacity %v", op)
		return
	}
}

func (b *builder) number(v cue.Value) {
	// Multiple conjuncts mostly means just additive constraints.
	// Type may be number of float.

	switch op, a := v.Expr(); op {
	case cue.LessThanOp:
		if b.ctx.exclusiveBool {
			b.setFilter("Schema", "exclusiveMaximum", ast.NewBool(true))
			b.setFilter("Schema", "maximum", b.big(a[0]))
		} else {
			b.setFilter("Schema", "exclusiveMaximum", b.big(a[0]))
		}

	case cue.LessThanEqualOp:
		b.setFilter("Schema", "maximum", b.big(a[0]))

	case cue.GreaterThanOp:
		if b.ctx.exclusiveBool {
			b.setFilter("Schema", "exclusiveMinimum", ast.NewBool(true))
			b.setFilter("Schema", "minimum", b.big(a[0]))
		} else {
			b.setFilter("Schema", "exclusiveMinimum", b.big(a[0]))
		}

	case cue.GreaterThanEqualOp:
		b.setFilter("Schema", "minimum", b.big(a[0]))

	case cue.NotEqualOp:
		i := b.big(a[0])
		b.setNot("allOff", ast.NewList(
			b.kv("minimum", i),
			b.kv("maximum", i),
		))

	case cue.CallOp:
		name := fmt.Sprint(a[0])
		switch name {
		case "math.MultipleOf":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "multipleOf", b.int(a[1]))

		default:
			b.unsupported(a[0])
			return
		}

	case cue.NoOp:
		// TODO: extract format from specific type.

	default:
		b.failf(v, "unsupported op for number %v", op)
	}
}

// Multiple Regexp conjuncts are represented as allOf all other
// constraints can be combined unless in the even of discontinuous
// lengths.

// string supports the following options:
//
// - maxLength (Unicode codepoints)
// - minLength (Unicode codepoints)
// - pattern (a regexp)
//
// The regexp pattern is as follows, and is limited to be a  strict subset of RE2:
// Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section-3.3
//
// JSON schema requires ECMA 262 regular expressions, but
// limited to the following constructs:
//   - simple character classes: [abc]
//   - range character classes: [a-z]
//   - complement character classes: [^abc], [^a-z]
//   - simple quantifiers: +, *, ?, and lazy versions +? *? ??
//   - range quantifiers: {x}, {x,y}, {x,}, {x}?, {x,y}?, {x,}?
//   - begin and end anchors: ^ and $
//   - simple grouping: (...)
//   - alteration: |
//
// This is a subset of RE2 used by CUE.
//
// Most notably absent:
//   - the '.' for any character (not sure if that is a doc bug)
//   - character classes \d \D [[::]] \pN \p{Name} \PN \P{Name}
//   - word boundaries
//   - capturing directives.
//   - flag setting
//   - comments
//
// The capturing directives and comments can be removed without
// compromising the meaning of the regexp (TODO). Removing
// flag setting will be tricky. Unicode character classes,
// boundaries, etc can be compiled into simple character classes,
// although the resulting regexp will look cumbersome.
func (b *builder) string(v cue.Value) {
	switch op, a := v.Expr(); op {

	case cue.RegexMatchOp, cue.NotRegexMatchOp:
		s, err := a[0].String()
		if err != nil {
			// TODO: this may be an unresolved interpolation or expression. Consider
			// whether it is reasonable to treat unevaluated operands as wholes and
			// generate a compound regular expression.
			b.failf(v, "regexp value must be a string: %v", err)
			return
		}
		if op == cue.RegexMatchOp {
			b.setFilter("Schema", "pattern", ast.NewString(s))
		} else {
			b.setNot("pattern", ast.NewString(s))
		}

	case cue.NoOp, cue.SelectorOp:

	case cue.CallOp:
		name := fmt.Sprint(a[0])
		switch name {
		case "strings.MinRunes":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "minLength", b.int(a[1]))
			return

		case "strings.MaxRunes":
			b.checkArgs(a, 1)
			b.setFilter("Schema", "maxLength", b.int(a[1]))
			return

		default:
			b.unsupported(a[0])
			return
		}

	default:
		b.failf(v, "unsupported op %v for string type", op)
	}
}

func (b *builder) bytes(v cue.Value) {
	switch op, a := v.Expr(); op {

	case cue.RegexMatchOp, cue.NotRegexMatchOp:
		s, err := a[0].Bytes()
		if err != nil {
			// TODO: this may be an unresolved interpolation or expression. Consider
			// whether it is reasonable to treat unevaluated operands as wholes and
			// generate a compound regular expression.
			b.failf(v, "regexp value must be of type bytes: %v", err)
			return
		}

		e := ast.NewString(string(s))
		if op == cue.RegexMatchOp {
			b.setFilter("Schema", "pattern", e)
		} else {
			b.setNot("pattern", e)
		}

		// TODO: support the following JSON schema constraints
		// - maxLength
		// - minLength

	case cue.NoOp, cue.SelectorOp:

	default:
		b.failf(v, "unsupported op %v for bytes type", op)
	}
}

type builder struct {
	ctx          *buildContext
	typ          string
	format       string
	singleFields *orderedMap
	current      *orderedMap
	allOf        []*ast.StructLit
	deprecated   bool

	// Building structural schema
	core       *builder
	kind       cue.Kind
	filled     *ast.StructLit
	values     []cue.Value // in structural mode, all values of not and *Of.
	keys       []string
	properties map[string]*builder
	items      *builder
}

func newRootBuilder(c *buildContext) *builder {
	return &builder{ctx: c}
}

func newOASBuilder(parent *builder) *builder {
	core := parent
	if parent.core != nil {
		core = parent.core
	}
	b := &builder{
		core:   core,
		ctx:    parent.ctx,
		typ:    parent.typ,
		format: parent.format,
	}
	return b
}

func (b *builder) isNonCore() bool {
	return b.core != nil
}

func (b *builder) setType(t, format string) {
	if b.typ == "" {
		b.typ = t
		if format != "" {
			b.format = format
		}
	}
}

func setType(t *orderedMap, b *builder) {
	if b.typ != "" {
		if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) {
			if !t.exists("type") {
				t.setExpr("type", ast.NewString(b.typ))
			}
		}
	}
	if b.format != "" {
		if b.core == nil || b.core.format != b.format {
			t.setExpr("format", ast.NewString(b.format))
		}
	}
}

// setFilter is like set, but allows the key-value pair to be filtered.
func (b *builder) setFilter(schema, key string, v ast.Expr) {
	if re := b.ctx.fieldFilter; re != nil && re.MatchString(path.Join(schema, key)) {
		return
	}
	b.set(key, v)
}

// setSingle sets a value of which there should only be one.
func (b *builder) setSingle(key string, v ast.Expr, drop bool) {
	if b.singleFields == nil {
		b.singleFields = &orderedMap{}
	}
	if b.singleFields.exists(key) {
		if !drop {
			b.failf(cue.Value{}, "more than one value added for key %q", key)
		}
	}
	b.singleFields.setExpr(key, v)
}

func (b *builder) set(key string, v ast.Expr) {
	if b.current == nil {
		b.current = &orderedMap{}
		b.allOf = append(b.allOf, (*ast.StructLit)(b.current))
	} else if b.current.exists(key) {
		b.current = &orderedMap{}
		b.allOf = append(b.allOf, (*ast.StructLit)(b.current))
	}
	b.current.setExpr(key, v)
}

func (b *builder) kv(key string, value ast.Expr) *ast.StructLit {
	return ast.NewStruct(key, value)
}

func (b *builder) setNot(key string, value ast.Expr) {
	b.add(ast.NewStruct("not", b.kv(key, value)))
}

func (b *builder) finish() *ast.StructLit {
	var t *orderedMap

	if b.filled != nil {
		return b.filled
	}
	switch len(b.allOf) {
	case 0:
		t = &orderedMap{}

	case 1:
		hasRef := false
		for _, e := range b.allOf[0].Elts {
			if f, ok := e.(*ast.Field); ok {
				name, _, _ := ast.LabelName(f.Label)
				hasRef = hasRef || name == "$ref"
			}
		}
		if !hasRef || b.singleFields == nil {
			t = (*orderedMap)(b.allOf[0])
			break
		}
		fallthrough

	default:
		exprs := []ast.Expr{}
		for _, s := range b.allOf {
			exprs = append(exprs, s)
		}
		t = &orderedMap{}
		t.setExpr("allOf", ast.NewList(exprs...))
	}
	if b.singleFields != nil {
		b.singleFields.Elts = append(b.singleFields.Elts, t.Elts...)
		t = b.singleFields
	}
	if b.deprecated {
		t.setExpr("deprecated", ast.NewBool(true))
	}
	setType(t, b)
	sortSchema((*ast.StructLit)(t))
	return (*ast.StructLit)(t)
}

func (b *builder) add(t *ast.StructLit) {
	b.allOf = append(b.allOf, t)
}

func (b *builder) addConjunct(f func(*builder)) {
	c := newOASBuilder(b)
	f(c)
	b.add(c.finish())
}

func (b *builder) addRef(v cue.Value, inst cue.Value, ref cue.Path) {
	name := b.ctx.makeRef(inst, ref)
	b.addConjunct(func(b *builder) {
		b.allOf = append(b.allOf, ast.NewStruct(
			"$ref",
			ast.NewString(path.Join("#", b.ctx.refPrefix, name)),
		))
	})

	if b.ctx.inst != inst {
		b.ctx.externalRefs[name] = &externalType{
			ref:   name,
			inst:  inst,
			path:  ref,
			value: v,
		}
	}
}

func (b *buildContext) makeRef(inst cue.Value, ref cue.Path) string {
	if b.nameFunc != nil {
		return b.nameFunc(inst, ref)
	}
	var buf strings.Builder
	for i, sel := range ref.Selectors() {
		if i > 0 {
			buf.WriteByte('.')
		}
		// TODO what should this do when it's not a valid identifier?
		buf.WriteString(selectorLabel(sel))
	}
	return buf.String()
}

func (b *builder) int64(v cue.Value) int64 {
	v, _ = v.Default()
	i, err := v.Int64()
	if err != nil {
		b.failf(v, "could not retrieve int: %v", err)
	}
	return i
}

func (b *builder) intExpr(i int64) ast.Expr {
	return &ast.BasicLit{
		Kind:  token.INT,
		Value: fmt.Sprint(i),
	}
}

func (b *builder) int(v cue.Value) ast.Expr {
	return b.intExpr(b.int64(v))
}

func (b *builder) inta(v cue.Value, offset int64) ast.Expr {
	return b.intExpr(b.int64(v) + offset)
}

func (b *builder) decode(v cue.Value) ast.Expr {
	v, _ = v.Default()
	return v.Syntax(cue.Final()).(ast.Expr)
}

func (b *builder) big(v cue.Value) ast.Expr {
	v, _ = v.Default()
	return v.Syntax(cue.Final()).(ast.Expr)
}

func selectorLabel(sel cue.Selector) string {
	if sel.Type().ConstraintType() == cue.PatternConstraint {
		return "*"
	}
	switch sel.LabelType() {
	case cue.StringLabel:
		return sel.Unquoted()
	case cue.DefinitionLabel:
		return sel.String()[1:]
	}
	// We shouldn't get anything other than non-hidden
	// fields and definitions because we've not asked the
	// Fields iterator for those or created them explicitly.
	panic(fmt.Sprintf("unreachable %v", sel.Type()))
}
