package jsonschema

import (
	_ "embed"
	"fmt"

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

//go:generate go tool cue exp gengotypes .

//go:embed crd.cue
var crdCUE []byte

// CRDConfig holds configuration for [ExtractCRDs].
// Although this empty currently, it allows configuration
// to be added in the future without breaking the API.
type CRDConfig struct{}

// ExtractedCRD holds an extracted Kubernetes CRD and the data it was derived from.
type ExtractedCRD struct {
	// Versions holds the CUE schemas extracted from the CRD: one per
	// version.
	Versions map[string]*ast.File

	// VersionToPath maps each version to the path
	// within Source containing the schema for that version.
	VersionToPath map[string]cue.Path

	// Data holds chosen fields extracted from the source CRD document.
	Data *CRDSpec

	// Source holds the raw CRD document from which Data is derived.
	Source cue.Value
}

// ExtractCRDs extracts Kubernetes custom resource definitions
// (CRDs) from the given data. If data holds an array, each element
// of the array might itself be a Kubernetes resource.
//
// While the data must hold Kubernetes resources, those resources
// need not all be CRDs: resources with a kind that's not "CustomResourceDefinition"
// will be ignored.
//
// If cfg is nil, it's equivalent to passing a pointer to the zero-valued [CRDConfig].
func ExtractCRDs(data cue.Value, cfg *CRDConfig) ([]*ExtractedCRD, error) {
	crdInfos, crdValues, err := decodeCRDSpecs(data)
	if err != nil {
		return nil, fmt.Errorf("cannot decode CRD: %v", err)
	}
	crds := make([]*ExtractedCRD, len(crdInfos))
	for crdIndex, crd := range crdInfos {
		versions := make(map[string]*ast.File)
		versionToPath := make(map[string]cue.Path)
		for i, version := range crd.Spec.Versions {
			rootPath := cue.MakePath(
				cue.Str("spec"),
				cue.Str("versions"),
				cue.Index(i),
				cue.Str("schema"),
				cue.Str("openAPIV3Schema"),
			)
			versionToPath[version.Name] = rootPath
			f, err := Extract(crdValues[crdIndex], &Config{
				PkgName: version.Name,
				// There are several kubernetes-related keywords that aren't implemented yet
				StrictFeatures: false,
				StrictKeywords: true,
				Root:           "#" + mustCUEPathToJSONPointer(rootPath),
				SingleRoot:     true,
				DefaultVersion: VersionKubernetesCRD,
			})
			if err != nil {
				return nil, err
			}
			namespaceConstraint := token.OPTION
			if crd.Spec.Scope == "Namespaced" {
				namespaceConstraint = token.NOT
			}
			// TODO provide a way to let this refer to a shared definition
			// in another package as the canonical definition for the schema.
			f.Decls = append(f.Decls,
				&ast.Field{
					Label: ast.NewIdent("apiVersion"),
					Value: ast.NewString(crd.Spec.Group + "/" + version.Name),
				},
				&ast.Field{
					Label: ast.NewIdent("kind"),
					Value: ast.NewString(crd.Spec.Names.Kind),
				},
				&ast.Field{
					Label:      ast.NewIdent("metadata"),
					Constraint: token.NOT,
					Value: ast.NewStruct(
						"name", token.NOT, ast.NewIdent("string"),
						"namespace", namespaceConstraint, ast.NewIdent("string"),
						// TODO inline struct lit
						"labels", token.OPTION, ast.NewStruct(
							ast.NewList(ast.NewIdent("string")), ast.NewIdent("string"),
						),
						"annotations", token.OPTION, ast.NewStruct(
							ast.NewList(ast.NewIdent("string")), ast.NewIdent("string"),
						),
						// The above fields aren't exhaustive.
						// TODO it would be nicer to refer to the actual #ObjectMeta
						// definition instead (and for that to be the case for embedded
						// resources in general) but that needs a deeper fix inside
						// encoding/jsonschema.
						&ast.Ellipsis{},
					),
				},
			)
			versions[version.Name] = f
		}
		crds[crdIndex] = &ExtractedCRD{
			Versions:      versions,
			VersionToPath: versionToPath,
			Data:          crdInfos[crdIndex],
			Source:        crdValues[crdIndex],
		}
	}
	return crds, nil
}

// decodeCRDSpecs decodes the CRD(s) held in the given value.
// It returns both the (partially) decoded CRDs and the values
// they were decoded from.
func decodeCRDSpecs(v cue.Value) ([]*CRDSpec, []cue.Value, error) {
	ctx := v.Context()

	// Check against the CUE version of the schema which can
	// do more detailed checks, including checking required fields,
	// before decoding into the Go struct.

	// TODO it would be nice to avoid compiling crdCUE every time, but
	// that's not possible given the restrictions on combining cue.Values
	// derived from different contexts.
	crdSchema := ctx.CompileBytes(crdCUE)
	crdSchema = crdSchema.FillPath(cue.MakePath(cue.Str("input")), v)
	specsv := crdSchema.LookupPath(cue.MakePath(cue.Str("specs")))
	if err := specsv.Err(); err != nil {
		return nil, nil, err
	}
	var specs []*CRDSpec
	var specsValues []cue.Value
	if err := specsv.Decode(&specs); err != nil {
		return nil, nil, err
	}
	if err := specsv.Decode(&specsValues); err != nil {
		return nil, nil, err
	}
	return specs, specsValues, nil
}
