/*
 * Copyright (C) 2022  Appvia Ltd <info@appvia.io>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package policies

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	terraformv1alpha1 "github.com/appvia/terranetes-controller/pkg/apis/terraform/v1alpha1"
	"github.com/appvia/terranetes-controller/pkg/utils"
)

type validator struct {
	cc client.Client
}

// NewValidator is validation handler
func NewValidator(cc client.Client) admission.CustomValidator {
	return &validator{cc: cc}
}

// ValidateDelete is called when a resource is being deleted
func (v *validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
	var warnings admission.Warnings

	list := &terraformv1alpha1.ConfigurationList{}
	o := obj.(*terraformv1alpha1.Policy)

	err := v.cc.List(ctx, list, client.InNamespace(""))
	if err != nil {
		return warnings, err
	}

	if o.GetAnnotations()[terraformv1alpha1.SkipDefaultsValidationCheck] == "true" {
		return warnings, nil
	}

	var using []string

	for _, x := range list.Items {
		items := strings.Split(x.GetAnnotations()[terraformv1alpha1.DefaultVariablesAnnotation], ",")
		if len(items) == 0 {
			continue
		}

		if utils.Contains(o.Name, items) {
			using = append(using, fmt.Sprintf("%s/%s", x.Namespace, x.Name))
		}
	}

	if len(using) > 0 {
		return warnings, fmt.Errorf("policy in use by configurations: %s", strings.Join(using, ", "))
	}

	return warnings, nil
}

// ValidateCreate is called when a new resource is created
func (v *validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
	return v.ValidateUpdate(ctx, nil, obj)
}

// ValidateUpdate is called when a resource is being updated
func (v *validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
	var warnings admission.Warnings
	o := newObj.(*terraformv1alpha1.Policy)

	if err := validateDefaultVariables(o); err != nil {
		return warnings, err
	}
	if err := validateCheckovConstraints(o); err != nil {
		return warnings, err
	}
	if err := validateModuleConstraint(o); err != nil {
		return warnings, err
	}

	return warnings, nil
}

// validateDefaultVariables ensures the default variables are valid
func validateDefaultVariables(policy *terraformv1alpha1.Policy) error {
	switch {
	case policy.Spec.Defaults == nil, len(policy.Spec.Defaults) == 0:
		return nil
	}

	for i := 0; i < len(policy.Spec.Defaults); i++ {
		x := policy.Spec.Defaults[i]

		if len(x.Variables.Raw) == 0 && len(x.Secrets) == 0 {
			return fmt.Errorf("spec.defaults[%d]: must specify at least one variable or secret", i)
		}
	}

	return nil
}

// validateModuleConstraint ensures the constraints are valid
func validateModuleConstraint(policy *terraformv1alpha1.Policy) error {
	switch {
	case policy.Spec.Constraints == nil, policy.Spec.Constraints.Modules == nil:
		return nil
	}

	constraint := policy.Spec.Constraints.Modules

	if constraint.Selector != nil {
		if constraint.Selector.Namespace != nil {
			if _, err := metav1.LabelSelectorAsSelector(constraint.Selector.Namespace); err != nil {
				return fmt.Errorf("spec.constraints.modules.selector.namespace is invalid, %w", err)
			}
		}

		if constraint.Selector.Resource != nil {
			if _, err := metav1.LabelSelectorAsSelector(constraint.Selector.Resource); err != nil {
				return fmt.Errorf("spec.constraints.modules.selector.resource is invalid, %w", err)
			}
		}
	}

	// @step: ensure the regexes are valid
	for i, x := range constraint.Allowed {
		if _, err := regexp.Compile(x); err != nil {
			return fmt.Errorf("spec.constraints.modules.allowed[%d] is invalid, %w", i, err)
		}
	}

	return nil
}

// validateCheckovConstraints ensures the constraints are valid
func validateCheckovConstraints(policy *terraformv1alpha1.Policy) error {
	switch {
	case policy.Spec.Constraints == nil, policy.Spec.Constraints.Checkov == nil:
		return nil
	}

	constraint := policy.Spec.Constraints.Checkov

	if constraint.Selector != nil {
		if constraint.Selector.Namespace != nil {
			if _, err := metav1.LabelSelectorAsSelector(constraint.Selector.Namespace); err != nil {
				return fmt.Errorf("spec.constraints.checkov.selector.namespace is invalid, %w", err)
			}
		}

		if constraint.Selector.Resource != nil {
			if _, err := metav1.LabelSelectorAsSelector(constraint.Selector.Resource); err != nil {
				return fmt.Errorf("spec.constraints.checkov.selector.resource is invalid, %w", err)
			}
		}
	}

	if len(constraint.Checks) > 0 && len(constraint.SkipChecks) > 0 {
		if utils.ContainsList(constraint.Checks, constraint.SkipChecks) {
			return errors.New("spec.constraints.checkov.skipChecks cannot contain checks from spec.constraints.checkov.checks")
		}
	}

	if constraint.Source != nil {
		if len(constraint.SkipChecks) > 0 {
			return errors.New("spec.constraints.checkov.skipChecks cannot be used with spec.constraints.checkov.source")
		}
		if len(constraint.Checks) > 0 {
			return errors.New("spec.constraints.checkov.checks cannot be used with spec.constraints.checkov.source")
		}
		if constraint.Source.Configuration == "" {
			return errors.New("spec.constraints.checkov.source.configuration is required")
		}
		if constraint.Source.URL == "" {
			return errors.New("spec.constraints.checkov.source.url is required")
		}
	}

	for i, external := range constraint.External {
		switch {
		case external.Name == "":
			return fmt.Errorf("spec.constraints.checkov.external[%d].name cannot be empty", i)
		case external.URL == "":
			return fmt.Errorf("spec.constraints.checkov.external[%d].url cannot be empty", i)
		case external.SecretRef != nil && external.SecretRef.Name == "":
			return fmt.Errorf("spec.constraints.checkov.external[%d].secretRef.name cannot be empty", i)
		case external.SecretRef != nil && external.SecretRef.Namespace != "":
			return fmt.Errorf("spec.constraints.checkov.external[%d].secretRef.namespace should not be set", i)
		}
	}

	return nil
}
