package main

import "fmt"

// cleanCRD does post-processing of the CRDs generated by kubebuilder to workaround limitations
// (e.g. https://github.com/kubernetes-sigs/controller-tools/issues/461)
func cleanCRD(filename string) {
	crd := ParseYaml(Read(filename))
	crd.RemoveNestedField("status")
	crd.RemoveNestedField("metadata", "annotations")
	crd.RemoveNestedField("metadata", "creationTimestamp")
	schema := crd.OpenAPIV3Schema()
	switch crd.Name() {
	case "cronworkflows.argoproj.io":
		patchMetadata(&schema, "spec", "properties", "workflowMetadata", "properties")
		patchWorkflowSpecTemplateFields(&schema, "spec", "properties", "workflowSpec", "properties")
	case "clusterworkflowtemplates.argoproj.io", "workflowtemplates.argoproj.io":
		patchWorkflowSpecTemplateFields(&schema, "spec", "properties")
	case "workflows.argoproj.io":
		patchWorkflowSpecTemplateFields(&schema, "spec", "properties")
		patchWorkflowSpecTemplateFields(&schema, "status", "properties", "storedWorkflowTemplateSpec", "properties")
		patchTemplateFields(&schema, "status", "properties", "storedTemplates", "additionalProperties", "properties")
		patchEphemeralVolumeFields(&schema, "status", "properties", "persistentVolumeClaims", "items", "properties")
		// Even with server-side apply, trying to apply
		// "manifests/base/crds/full/argoproj.io_workflows.yaml" results in
		// either the error "Request entity too large: limit is 3145728" or the
		// error "Error from server: rpc error: code = ResourceExhausted desc =
		// trying to send message larger than max (2163613 vs. 2097152)".
		//
		// The former error is from the value of "MaxRequestBodyBytes" in the
		// Kubernetes API server, which defaults to 3MB:
		// https://github.com/kubernetes/kubernetes/blob/f47e9696d7237f1011d23c9b55f6947e60526179/staging/src/k8s.io/apiserver/pkg/server/config.go#L456-L465
		//
		// The latter error is from the gRPC client, which defaults to 2MB:
		// https://github.com/kubernetes/kubernetes/blob/f47e9696d7237f1011d23c9b55f6947e60526179/vendor/google.golang.org/grpc/clientconn.go#L102
		//
		// To workaround that, remove the "description" attribute from every
		// field under "status" and "spec.properties.templateDefaults", since
		// those contain a lot of fields that are rarely referenced.
		schema.RecursiveRemoveDescriptions("status")
		schema.RecursiveRemoveDescriptions("spec", "properties", "templateDefaults")
	case "workfloweventbindings.argoproj.io":
		patchMetadata(&schema, "spec", "properties", "submit", "properties", "metadata", "properties")
	case "workflowtasksets.argoproj.io":
		patchVolumeFields(&schema, "spec", "properties", "tasks", "additionalProperties", "properties")
	}
	crd.WriteYaml(filename)
}

func patchWorkflowSpecTemplateFields(schema *obj, baseFields ...string) {
	patchTemplateFields(schema, append(baseFields, "templateDefaults", "properties")...)
	patchTemplateFields(schema, append(baseFields, "templates", "items", "properties")...)
	patchVolumeFields(schema, baseFields...)
	patchMetadata(schema, append(baseFields, "volumeClaimTemplates", "items", "properties", "metadata", "properties")...)
}

// Workaround for missing generateName (see https://github.com/kubernetes-sigs/controller-tools/pull/640)
func patchMetadata(schema *obj, baseFields ...string) {
	schema.SetNestedField(map[string]string{"type": "string"}, append(baseFields, "generateName")...)
}

func patchEphemeralVolumeFields(schema *obj, baseFields ...string) {
	patchMetadata(schema, append(baseFields, "ephemeral", "properties", "volumeClaimTemplate", "properties", "metadata", "properties")...)
}

func patchVolumeFields(schema *obj, baseFields ...string) {
	patchEphemeralVolumeFields(schema, append(baseFields, "volumes", "items", "properties")...)
}

func patchTemplateFields(schema *obj, baseFields ...string) {
	patchVolumeFields(schema, baseFields...)
	// "container" and "script" templates embed the k8s.io/api/core/v1/Container
	// struct, which requires the "name" field to be specified, but it's not actually required,
	// and there's no easy way to override validating rules for embedded structs in kubebuilder.
	schema.RemoveNestedField(append(baseFields, "container", "required")...)
	schema.RemoveNestedField(append(baseFields, "script", "required")...)

	// Hack to transform "steps" from an array of objects, e.g.
	//   steps:
	//   - steps:
	//     - name: foo
	//       template: bar
	// to an array of arrays, e.g.:
	//   steps:
	//   - - name: foo
	//       template: bar
	//
	// The reason it's represented as an array of objects in "workflow_types.go"
	// is because that's the only way to get kubebuilder to generate the spec
	// without breaking API compatibility.
	stepFields := append(baseFields, "steps", "items")
	schema.CopyNestedField(append(stepFields, "properties", "steps"), stepFields)
}

// minimizeCRD generates a stripped-down CRD as a workaround for "Request entity too large: limit is 3145728" errors due to https://github.com/kubernetes/kubernetes/issues/82292.
// This isn't an issue if you use server-side apply, but not everyone has that available.
func minimizeCRD(filename string) {
	data := Read(filename)
	shouldMinimize := false
	// TODO: The 512KB limit is just to avoid changing too much in https://github.com/argoproj/argo-workflows/pull/14044
	// Increase this back to 1MB after that's merged.
	if len(data) > 512*1024 {
		fmt.Printf("Minimizing %s due to CRD size (%d) exceeding 512KB\n", filename, len(data))
		shouldMinimize = true
	}
	if !shouldMinimize {
		return
	}
	crd := ParseYaml(data)
	stripSpecAndStatusFields(crd)
	crd.WriteYaml(filename)
}

// stripSpecAndStatusFields strips the "spec" and "status" fields from the CRD, as those are usually the largest.
func stripSpecAndStatusFields(crd *obj) {
	schema := crd.OpenAPIV3Schema()
	preserveMarker := obj{"type": "object", "x-kubernetes-preserve-unknown-fields": true, "x-kubernetes-map-type": "atomic"}
	if _, ok := schema["spec"]; ok {
		schema["spec"] = preserveMarker
	}
	if _, ok := schema["status"]; ok {
		schema["status"] = preserveMarker
	}
}
