// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package setters2

import (
	"strings"

	"github.com/go-openapi/spec"
	"sigs.k8s.io/kustomize/kyaml/errors"
	"sigs.k8s.io/kustomize/kyaml/fieldmeta"
	"sigs.k8s.io/kustomize/kyaml/openapi"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

// Delete delete openAPI definition references from resource fields.
type Delete struct {
	// Name is the name of the openAPI definition to delete.
	Name string

	// DefinitionPrefix is the prefix of the OpenAPI definition type
	DefinitionPrefix string

	SettersSchema *spec.Schema
}

// Filter implements yaml.Filter
func (d *Delete) Filter(object *yaml.RNode) (*yaml.RNode, error) {
	return object, accept(d, object, d.SettersSchema)
}

func (d *Delete) visitSequence(_ *yaml.RNode, _ string, _ *openapi.ResourceSchema) error {
	// no-op
	return nil
}

func (d *Delete) visitMapping(object *yaml.RNode, _ string, _ *openapi.ResourceSchema) error {
	fieldRNodes, err := object.FieldRNodes()
	if err != nil {
		return err
	}

	// for each of the field node key visit it as scalar to delete the array setter comment
	for _, fieldRNode := range fieldRNodes {
		err := d.visitScalar(fieldRNode, "", nil, nil)
		if err != nil {
			return err
		}
	}
	return nil
}

// visitScalar implements visitor
// visitScalar will remove the reference on each scalar field whose name matches.
func (d *Delete) visitScalar(object *yaml.RNode, _ string, _, _ *openapi.ResourceSchema) error {
	// read the field metadata
	fm := fieldmeta.FieldMeta{SettersSchema: d.SettersSchema}
	if err := fm.Read(object); err != nil {
		return err
	}

	// Delete the reference iff the ref string matches with DefinitionPrefix
	if strings.HasSuffix(fm.Schema.Ref.String(), d.DefinitionPrefix+d.Name) {
		// remove the ref on the metadata
		fm.Schema.Ref = spec.Ref{}

		// write the field metadata
		if err := fm.Write(object); err != nil {
			return err
		}
	}

	return nil
}

// DeleterDefinition may be used to update a files OpenAPI definitions with a new setter.
type DeleterDefinition struct {
	// Name is the name of the openAPI definition to delete.
	Name string `yaml:"name"`

	// DefinitionPrefix is the prefix of the OpenAPI definition type
	DefinitionPrefix string `yaml:"definitionPrefix"`
}

func (dd DeleterDefinition) DeleteFromFile(path string) error {
	return yaml.UpdateFile(dd, path)
}

// SubstReferringDefinition check if the definition used in substitution and return the substitution name if true
func SubstReferringDefinition(definitions *yaml.RNode, key string) string {
	fieldNames, err := definitions.Fields()
	if err != nil {
		return ""
	}
	for _, fieldName := range fieldNames {
		// the definition key -- contains the substitution name
		subkey := definitions.Field(fieldName).Key.YNode().Value
		if strings.HasPrefix(subkey, fieldmeta.SubstitutionDefinitionPrefix) {
			substNode, err := definitions.Field(fieldName).Value.Pipe(yaml.Lookup(K8sCliExtensionKey, "substitution"))
			if err != nil {
				continue
			}

			b, err := substNode.MarshalJSON()
			if err != nil {
				continue
			}

			subst := SubstitutionDefinition{}
			if err := yaml.Unmarshal(b, &subst); err != nil {
				continue
			}
			// Check the ref in value to see if it contains the setter key
			for _, v := range subst.Values {
				if strings.HasSuffix(v.Ref, key) {
					return subst.Name
				}
			}
		}
	}

	return ""
}

func (dd DeleterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) {
	key := dd.DefinitionPrefix + dd.Name
	var defType string

	switch dd.DefinitionPrefix {
	case fieldmeta.SubstitutionDefinitionPrefix:
		defType = "substitution"
	case fieldmeta.SetterDefinitionPrefix:
		defType = "setter"
	default:
		return nil, errors.Errorf("the input delete definitionPrefix does't match any of openAPI definitions, "+
			"allowed values [%s, %s]", fieldmeta.SetterDefinitionPrefix, fieldmeta.SubstitutionDefinitionPrefix)
	}

	definitions, err := object.Pipe(yaml.Lookup(openapi.SupplementaryOpenAPIFieldName, "definitions"))
	if err != nil {
		return nil, err
	}
	// return error if the setter to be deleted doesn't exist
	if definitions == nil || definitions.Field(key) == nil {
		return nil, errors.Errorf("%s %q does not exist", defType, dd.Name)
	}

	subst := SubstReferringDefinition(definitions, key)

	if subst != "" {
		return nil, errors.Errorf("%s %q is used in substitution %q, please delete the parent substitution first", defType, dd.Name, subst)
	}

	_, err = definitions.Pipe(yaml.FieldClearer{Name: key})
	if err != nil {
		return nil, err
	}
	// remove definitions if it's empty
	_, err = object.Pipe(yaml.Lookup(openapi.SupplementaryOpenAPIFieldName), yaml.FieldClearer{Name: "definitions", IfEmpty: true})
	if err != nil {
		return nil, err
	}

	// remove openApi if it's empty
	_, err = object.Pipe(yaml.FieldClearer{Name: openapi.SupplementaryOpenAPIFieldName, IfEmpty: true})
	if err != nil {
		return nil, err
	}

	return object, nil
}
