package apistore

import (
	"context"
	"fmt"

	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	secret "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)

// prepareSecureValues will create any new secure values and register changes inside the provided objectForStorage
// any call to this function MUST be followed by a call to info.finish(ctx, nil, store) to ensure that the secure values are cleaned up
func prepareSecureValues(ctx context.Context, store secret.InlineSecureValueSupport, obj utils.GrafanaMetaAccessor, previousObject utils.GrafanaMetaAccessor, v *objectForStorage) (err error) {
	secure, err := obj.GetSecureValues()
	if err != nil {
		return err
	}

	// Owner reference for inline values
	v.ref = utils.ToObjectReference(obj)

	var previous common.InlineSecureValues
	if previousObject == nil {
		if len(secure) == 0 {
			return nil // nothing needs to change
		}
		if store == nil {
			return fmt.Errorf("secure value support is not configured (create)")
		}
		previous = make(common.InlineSecureValues, 0)
	} else {
		// Merge in any values from the previous object and handle remove
		previous, err = previousObject.GetSecureValues()
		if err != nil {
			return err
		}
		for _, p := range previous {
			if p.Name == "" || p.Remove || !p.Create.IsZero() {
				return fmt.Errorf("invalid state, saved values must only have a name")
			}
		}

		// Keep exactly what we had before
		if len(secure) == 0 {
			if len(previous) > 0 {
				return obj.SetSecureValues(previous)
			}
			return nil
		}

		if store == nil {
			return fmt.Errorf("secure value support is not configured (update)")
		}
	}

	for k, val := range secure {
		before := previous[k]
		if val.Name == "" {
			if before.Name != "" { // implicitly delete previous secure value if the same field no longer references it
				v.deleteSecureValues = append(v.deleteSecureValues, before.Name)
				delete(previous, k)
			}
			if val.Remove {
				if before.Name == "" {
					return fmt.Errorf("cannot remove secure value '%s', it did not exist in the previous value", k)
				}
				delete(secure, k)
				v.hasChanged = true
				continue
			}
			if !val.Create.IsZero() {
				n, err := store.CreateInline(ctx, v.ref, val.Create)
				if err != nil {
					return err
				}
				v.createdSecureValues = append(v.createdSecureValues, n)
				v.hasChanged = true
				secure[k] = common.InlineSecureValue{Name: n}
				continue
			}
			return fmt.Errorf("invalid secure value state: %s", k)
		}

		// The name changed from the previously stored value
		if before.Name != "" && before.Name != val.Name {
			// This can happen when explicitly shifting from an inline value to a shared secret
			v.deleteSecureValues = append(v.deleteSecureValues, before.Name)
			v.hasChanged = true
		}

		delete(previous, k)
	}

	// Keep all previous values that were not referenced in the update
	for k, v := range previous {
		_, found := secure[k]
		if !found {
			secure[k] = v // the previous value
		}
	}

	return cleanupSecureValues(v, obj, secure)
}

// make sure the registered changes are unique and valid
func cleanupSecureValues(v *objectForStorage, obj utils.GrafanaMetaAccessor, secure common.InlineSecureValues) error {
	// Make sure the deleted list is unique and does not contain any referenced values
	if len(v.deleteSecureValues) > 0 && len(secure) > 0 {
		confirm := v.deleteSecureValues
		v.deleteSecureValues = make([]string, 0, len(v.deleteSecureValues))
		used := make(map[string]bool, len(secure))
		for _, v := range secure {
			used[v.Name] = true
		}
		for _, name := range confirm {
			if _, ok := used[name]; ok {
				continue
			}
			used[name] = true
			v.deleteSecureValues = append(v.deleteSecureValues, name)
		}
	}

	if len(v.deleteSecureValues) > 0 || len(v.createdSecureValues) > 0 {
		v.hasChanged = true
	}
	return obj.SetSecureValues(secure)
}

// Mutation hook that will delete secure values
func handleSecureValuesDelete(ctx context.Context, store secret.InlineSecureValueSupport, obj utils.GrafanaMetaAccessor) error {
	secure, err := obj.GetSecureValues()
	if err != nil || len(secure) == 0 {
		return err
	}

	if store == nil {
		return fmt.Errorf("secure value support is not configured (delete)")
	}

	owner := utils.ToObjectReference(obj)
	for _, v := range secure {
		if err = store.DeleteWhenOwnedByResource(ctx, owner, v.Name); err != nil {
			return err
		}
	}
	return obj.SetSecureValues(nil) // remove them from the object
}
