// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package controllers

import (
	"context"
	"crypto/x509"
	"fmt"
	"time"

	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/sets"
	clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1"
	controllerruntime "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
	"github.com/clastix/kamaji/controllers/utils"
	"github.com/clastix/kamaji/internal/constants"
	"github.com/clastix/kamaji/internal/crypto"
	"github.com/clastix/kamaji/internal/utilities"
)

type CertificateLifecycle struct {
	Channel  chan event.GenericEvent
	Deadline time.Duration

	client client.Client
}

func (s *CertificateLifecycle) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
	logger := log.FromContext(ctx)

	logger.Info("starting CertificateLifecycle handling")

	var secret corev1.Secret
	if err := s.client.Get(ctx, request.NamespacedName, &secret); err != nil {
		if k8serrors.IsNotFound(err) {
			logger.Info("resource may have been deleted, skipping")

			return reconcile.Result{}, nil
		}

		logger.Error(err, "cannot retrieve the required resource")

		return reconcile.Result{}, err
	}

	if utils.IsPaused(&secret) {
		logger.Info("paused reconciliation, no further actions")

		return reconcile.Result{}, nil
	}

	checkType, ok := secret.GetLabels()[constants.ControllerLabelResource]
	if !ok {
		logger.Info("missing controller label, shouldn't happen")

		return reconcile.Result{}, nil
	}

	var crt *x509.Certificate
	var err error

	switch checkType {
	case utilities.CertificateX509Label:
		crt, err = s.extractCertificateFromBareSecret(secret)
	case utilities.CertificateKubeconfigLabel:
		crt, err = s.extractCertificateFromKubeconfig(secret)
	default:
		return reconcile.Result{}, fmt.Errorf("unsupported strategy, %q", checkType)
	}

	if err != nil {
		logger.Error(err, "skipping reconciliation")

		return reconcile.Result{}, nil
	}

	deadline := time.Now().Add(s.Deadline)

	if deadline.After(crt.NotAfter) {
		logger.Info("certificate near expiration, must be rotated")

		s.Channel <- event.GenericEvent{Object: &kamajiv1alpha1.TenantControlPlane{
			ObjectMeta: metav1.ObjectMeta{
				Name:      secret.GetOwnerReferences()[0].Name,
				Namespace: secret.Namespace,
			},
		}}

		logger.Info("certificate rotation triggered")

		return reconcile.Result{}, nil
	}

	after := crt.NotAfter.Sub(deadline)

	logger.Info("certificate is still valid, enqueuing back", "after", after.String())

	return reconcile.Result{RequeueAfter: after}, nil
}

func (s *CertificateLifecycle) extractCertificateFromBareSecret(secret corev1.Secret) (*x509.Certificate, error) {
	var crt *x509.Certificate
	var err error

	for _, v := range secret.Data {
		if crt, err = crypto.ParseCertificateBytes(v); err == nil {
			break
		}
	}

	if crt == nil {
		return nil, fmt.Errorf("none of the provided keys is containing a valid x509 certificate")
	}

	return crt, nil
}

func (s *CertificateLifecycle) extractCertificateFromKubeconfig(secret corev1.Secret) (*x509.Certificate, error) {
	var kc *clientcmdapiv1.Config
	var err error

	for k := range secret.Data {
		if kc, err = utilities.DecodeKubeconfig(secret, k); err == nil {
			break
		}
	}

	if kc == nil {
		return nil, fmt.Errorf("none of the provided keys is containing a valid kubeconfig")
	}

	crt, err := crypto.ParseCertificateBytes(kc.AuthInfos[0].AuthInfo.ClientCertificateData)
	if err != nil {
		return nil, errors.Wrap(err, "cannot parse kubeconfig certificate bytes")
	}

	return crt, nil
}

func (s *CertificateLifecycle) SetupWithManager(mgr controllerruntime.Manager) error {
	s.client = mgr.GetClient()

	supportedStrategies := sets.New[string](utilities.CertificateX509Label, utilities.CertificateKubeconfigLabel)

	return controllerruntime.NewControllerManagedBy(mgr).
		For(&corev1.Secret{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
			labels := object.GetLabels()

			if labels == nil {
				return false
			}

			value, ok := labels[constants.ControllerLabelResource]
			if !ok {
				return false
			}

			return supportedStrategies.Has(value)
		}))).
		Complete(s)
}
