package k8s

import (
	"context"
	"fmt"

	log "github.com/sirupsen/logrus"
	admissionv1 "k8s.io/api/admissionregistration/v1"
	v1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
)

type k8s struct {
	clientset           kubernetes.Interface
	aggregatorClientset clientset.Interface
}

func New(clientset kubernetes.Interface, aggregatorClientset clientset.Interface) *k8s {
	if clientset == nil {
		log.Fatal("no kubernetes client given")
	}

	if aggregatorClientset == nil {
		log.Fatal("no kubernetes aggregator client given")
	}

	return &k8s{
		clientset:           clientset,
		aggregatorClientset: aggregatorClientset,
	}
}

type PatchOptions struct {
	ValidatingWebhookConfigurationName string
	MutatingWebhookConfigurationName   string
	APIServiceName                     string
	CABundle                           []byte
	FailurePolicyType                  admissionv1.FailurePolicyType
}

func (k8s *k8s) PatchObjects(ctx context.Context, options PatchOptions) error {
	patchMutating := options.MutatingWebhookConfigurationName != ""
	patchValidating := options.ValidatingWebhookConfigurationName != ""
	patchAPIService := options.APIServiceName != ""

	if !patchMutating && !patchValidating && options.FailurePolicyType != "" {
		return fmt.Errorf("failurePolicy specified, but no webhook will be patched")
	}

	if patchMutating && patchValidating &&
		options.MutatingWebhookConfigurationName != options.ValidatingWebhookConfigurationName {
		return fmt.Errorf("webhook names must be the same")
	}

	if patchAPIService {
		log.Infof("patching APIService %q", options.APIServiceName)

		if err := k8s.patchAPIService(ctx, options.APIServiceName, options.CABundle); err != nil {
			// Intentionally don't wrap error here to preserve old behavior and be able to log both
			// original error and a message.
			return err
		}
	}

	webhookName := options.ValidatingWebhookConfigurationName
	if webhookName == "" {
		webhookName = options.MutatingWebhookConfigurationName
	}

	if patchMutating || patchValidating {
		return k8s.patchWebhookConfigurations(ctx, webhookName, options.CABundle, options.FailurePolicyType, patchMutating, patchValidating)
	}

	return nil
}

func (k8s *k8s) patchAPIService(ctx context.Context, objectName string, ca []byte) error {
	log.Infof("patching APIService %q", objectName)

	c := k8s.aggregatorClientset.ApiregistrationV1().APIServices()

	apiService, err := c.Get(ctx, objectName, metav1.GetOptions{})
	if err != nil {
		return &wrappedError{
			err:     err,
			message: fmt.Sprintf("failed getting APIService %q", objectName),
		}
	}

	apiService.Spec.CABundle = ca
	apiService.Spec.InsecureSkipTLSVerify = false

	if _, err := c.Update(ctx, apiService, metav1.UpdateOptions{}); err != nil {
		return &wrappedError{
			err:     err,
			message: fmt.Sprintf("failed patching APIService %q", objectName),
		}
	}

	log.Debug("patched APIService")

	return nil
}

// PatchWebhookConfigurations will patch validatingWebhook and mutatingWebhook clientConfig configurations with
// the provided ca data. If failurePolicy is provided, patch all webhooks with this value
func (k8s *k8s) patchWebhookConfigurations(
	ctx context.Context,
	configurationName string,
	ca []byte,
	failurePolicy admissionv1.FailurePolicyType,
	patchMutating bool,
	patchValidating bool,
) error {
	log.Infof("patching webhook configurations '%s' mutating=%t, validating=%t, failurePolicy=%s", configurationName, patchMutating, patchValidating, failurePolicy)

	if patchValidating {
		if err := k8s.patchValidating(ctx, configurationName, ca, failurePolicy); err != nil {
			// Intentionally don't wrap error here to preserve old behavior and be able to log both original error and a message.
			return err
		}
	} else {
		log.Debug("validating hook patching not required")
	}

	if patchMutating {
		if err := k8s.patchMutating(ctx, configurationName, ca, failurePolicy); err != nil {
			// Intentionally don't wrap error here to preserve old behavior and be able to log both original error and a message.
			return err
		}
	} else {
		log.Debug("mutating hook patching not required")
	}

	log.Info("Patched hook(s)")

	return nil
}

type wrappedError struct {
	err     error
	message string
}

func (err wrappedError) Error() string {
	return err.message
}

func (err wrappedError) Unwrap() error {
	return err.err
}

func (k8s *k8s) patchValidating(ctx context.Context, configurationName string, ca []byte, failurePolicy admissionv1.FailurePolicyType) error {
	valHook, err := k8s.clientset.
		AdmissionregistrationV1().
		ValidatingWebhookConfigurations().
		Get(ctx, configurationName, metav1.GetOptions{})
	if err != nil {
		return &wrappedError{
			err:     err,
			message: "failed getting validating webhook",
		}
	}

	for i := range valHook.Webhooks {
		h := &valHook.Webhooks[i]
		h.ClientConfig.CABundle = ca
		if failurePolicy != "" {
			h.FailurePolicy = &failurePolicy
		}
	}

	if _, err = k8s.clientset.AdmissionregistrationV1().
		ValidatingWebhookConfigurations().
		Update(ctx, valHook, metav1.UpdateOptions{}); err != nil {
		return &wrappedError{
			err:     err,
			message: "failed patching validating webhook",
		}
	}
	log.Debug("patched validating hook")

	return nil
}

func (k8s *k8s) patchMutating(ctx context.Context, configurationName string, ca []byte, failurePolicy admissionv1.FailurePolicyType) error {
	mutHook, err := k8s.clientset.
		AdmissionregistrationV1().
		MutatingWebhookConfigurations().
		Get(ctx, configurationName, metav1.GetOptions{})
	if err != nil {
		return &wrappedError{
			err:     err,
			message: "failed getting mutating webhook",
		}
	}

	for i := range mutHook.Webhooks {
		h := &mutHook.Webhooks[i]
		h.ClientConfig.CABundle = ca
		if failurePolicy != "" {
			h.FailurePolicy = &failurePolicy
		}
	}

	if _, err = k8s.clientset.AdmissionregistrationV1().
		MutatingWebhookConfigurations().
		Update(ctx, mutHook, metav1.UpdateOptions{}); err != nil {
		return &wrappedError{
			err:     err,
			message: "failed patching mutating webhook",
		}
	}
	log.Debug("patched mutating hook")

	return nil
}

// GetCaFromSecret will check for the presence of a secret. If it exists, will return the content of the
// "ca" from the secret, otherwise will return nil
func (k8s *k8s) GetCaFromSecret(ctx context.Context, secretName string, namespace string) []byte {
	log.Debugf("getting secret '%s' in namespace '%s'", secretName, namespace)
	secret, err := k8s.clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
	if err != nil {
		if k8serrors.IsNotFound(err) {
			log.WithField("err", err).Info("no secret found")
			return nil
		}
		log.WithField("err", err).Fatal("error getting secret")
	}

	data := secret.Data["ca"]
	if data == nil {
		log.Fatal("got secret, but it did not contain a 'ca' key")
	}
	log.Debug("got secret")
	return data
}

// SaveCertsToSecret saves the provided ca, cert and key into a secret in the specified namespace.
func (k8s *k8s) SaveCertsToSecret(ctx context.Context, secretName, namespace, certName, keyName string, ca, cert, key []byte) {
	log.Debugf("saving to secret '%s' in namespace '%s'", secretName, namespace)
	secret := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: secretName,
		},
		Data: map[string][]byte{"ca": ca, certName: cert, keyName: key},
	}

	log.Debug("saving secret")
	_, err := k8s.clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
	if err != nil {
		log.WithField("err", err).Fatal("failed creating secret")
	}
	log.Debug("saved secret")
}
