package validator

import (
	"errors"
	"strconv"
	"strings"

	"k8s.io/apimachinery/pkg/util/validation"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/apiserver/pkg/admission"

	secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
	"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)

type secureValueValidator struct{}

var _ contracts.SecureValueValidator = &secureValueValidator{}

func ProvideSecureValueValidator() contracts.SecureValueValidator {
	return &secureValueValidator{}
}

func (v *secureValueValidator) Validate(sv, oldSv *secretv1beta1.SecureValue, operation admission.Operation) field.ErrorList {
	errs := make(field.ErrorList, 0)

	// Operation-specific field validation.
	switch operation {
	case admission.Create:
		errs = validateSecureValueCreate(sv)

	// If we plan to support PATCH-style updates, we shouldn't be requiring fields to be set.
	case admission.Update:
		errs = validateSecureValueUpdate(sv, oldSv)

	case admission.Delete:
	case admission.Connect:
	}

	// General validations.
	if err := validation.IsDNS1123Subdomain(sv.Name); len(err) > 0 {
		errs = append(
			errs,
			field.Invalid(field.NewPath("metadata", "name"), sv.Name, strings.Join(err, ",")),
		)

		return errs
	}
	if err := validation.IsDNS1123Subdomain(sv.Namespace); len(err) > 0 {
		errs = append(
			errs,
			field.Invalid(field.NewPath("metadata", "namespace"), sv.Name, strings.Join(err, ",")),
		)

		return errs
	}

	if sv.Spec.Value != nil && len(*sv.Spec.Value) > contracts.SecureValueRawInputMaxSizeBytes {
		errs = append(
			errs,
			field.TooLong(field.NewPath("spec", "value"), len(*sv.Spec.Value), contracts.SecureValueRawInputMaxSizeBytes),
		)
	}

	if errs := validateDecrypters(sv.Spec.Decrypters); len(errs) > 0 {
		return errs
	}

	return errs
}

// validateSecureValueCreate does basic spec validation of a securevalue for the Create operation.
func validateSecureValueCreate(sv *secretv1beta1.SecureValue) field.ErrorList {
	errs := make(field.ErrorList, 0)

	if sv.Spec.Description == "" {
		errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required"))
	}

	if (sv.Spec.Value == nil || (sv.Spec.Value != nil && *sv.Spec.Value == "")) && (sv.Spec.Ref == nil || (sv.Spec.Ref != nil && *sv.Spec.Ref == "")) {
		errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required"))
	}

	if (sv.Spec.Value != nil && *sv.Spec.Value != "") && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
		errs = append(errs, field.Forbidden(field.NewPath("spec"), "only one of `value` or `ref` can be set"))
	}

	return errs
}

// validateSecureValueUpdate does basic spec validation of a securevalue for the Update operation.
func validateSecureValueUpdate(sv, oldSv *secretv1beta1.SecureValue) field.ErrorList {
	errs := make(field.ErrorList, 0)

	// For updates, an `old` object is required.
	if oldSv == nil {
		errs = append(errs, field.InternalError(field.NewPath("spec"), errors.New("old object is nil")))

		return errs
	}

	// Only validate if one of the fields is being changed/set.
	if (sv.Spec.Value != nil && *sv.Spec.Value != "") || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
		if (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref != "") && (sv.Spec.Value != nil && *sv.Spec.Value != "") {
			errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set"))
		}

		if (oldSv.Spec.Ref == nil || (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref == "")) && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
			errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `ref` when `value` was already previously set"))
		}
	}

	return errs
}

// validateDecrypters validates that (if populated) the `decrypters` must be unique.
func validateDecrypters(decrypters []string) field.ErrorList {
	errs := make(field.ErrorList, 0)

	// Limit the number of decrypters to 64 to not have it unbounded.
	// The number was chosen arbitrarily and should be enough.
	if len(decrypters) > 64 {
		errs = append(
			errs,
			field.TooMany(field.NewPath("spec", "decrypters"), len(decrypters), 64),
		)

		return errs
	}

	decrypterNames := make(map[string]struct{}, 0)

	for i, decrypter := range decrypters {
		decrypter = strings.TrimSpace(decrypter)
		if decrypter == "" {
			errs = append(
				errs,
				field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"),
			)

			continue
		}

		// Use the same validation as labels for the decrypters.
		if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 {
			for _, verr := range verrs {
				errs = append(
					errs,
					field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr),
				)
			}

			continue
		}

		if _, exists := decrypterNames[decrypter]; exists {
			errs = append(
				errs,
				field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"),
			)

			continue
		}

		decrypterNames[decrypter] = struct{}{}
	}

	return errs
}
