/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package webhook

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/klog/v2"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/manager"

	"github.com/apache/incubator-uniffle/deploy/kubernetes/operator/pkg/utils"
	"github.com/apache/incubator-uniffle/deploy/kubernetes/operator/pkg/webhook/config"
	webhookconstants "github.com/apache/incubator-uniffle/deploy/kubernetes/operator/pkg/webhook/constants"
	"github.com/apache/incubator-uniffle/deploy/kubernetes/operator/pkg/webhook/inspector"
	"github.com/apache/incubator-uniffle/deploy/kubernetes/operator/pkg/webhook/syncer"
)

const (
	certsSecretName = "rss-webhook-certs"

	serverCert = "server.crt"
	serverKey  = "server.key"
	caCert     = "ca.crt"
)

// AdmissionManager manages admission webhook server for shuffle servers.
type AdmissionManager interface {
	manager.Runnable
}

// NewAdmissionManager creates an AdmissionManager.
func NewAdmissionManager(cfg *config.Config) AdmissionManager {
	return newAdmissionManager(cfg)
}

// newAdmissionManager creates an admissionManager.
func newAdmissionManager(cfg *config.Config) *admissionManager {
	am := &admissionManager{
		externalService: cfg.ExternalService,
		kubeClient:      cfg.KubeClient,
	}
	if !cfg.NeedLoadCertsFromSecret() {
		am.caCertBody = cfg.GetCaCert()
		am.tlsConfig = cfg.TLSConfig()
	} else {
		am.loadCertsFromSecret()
	}

	klog.Infof("leader election enabled?: %v", cfg.LeaderElection)

	mgr, err := ctrl.NewManager(cfg.RESTConfig, ctrl.Options{
		LeaderElection:          cfg.LeaderElection,
		LeaderElectionID:        cfg.LeaderElectionID(),
		LeaderElectionNamespace: utils.GetCurrentNamespace(),
	})
	if err != nil {
		klog.Fatalf("build manager for admission manager failed: %v", err)
	}
	am.mgr = mgr
	am.syncer = syncer.NewConfigSyncer(am.caCertBody, cfg.ExternalService, cfg.KubeClient)
	am.inspector = inspector.NewInspector(cfg, am.tlsConfig)
	return am
}

// admissionManager implements the AdmissionManager interface.
type admissionManager struct {
	externalService string
	caCertBody      []byte
	tlsConfig       *tls.Config

	kubeClient kubernetes.Interface

	mgr       manager.Manager
	syncer    syncer.ConfigSyncer
	inspector inspector.Inspector
}

// Start starts the AdmissionManager.
func (am *admissionManager) Start(ctx context.Context) error {
	stopCh := ctx.Done()
	if err := am.mgr.Add(am.syncer); err != nil {
		klog.Errorf("add syncer to mgr of admission manager failed: %v", err)
		return err
	}
	go func() {
		if err := am.mgr.Start(ctx); err != nil {
			klog.Fatalf("mgr of admission manager started failed: %v", err)
		}
	}()
	go func() {
		if err := am.inspector.Start(ctx); err != nil {
			klog.Fatalf("start webhook server failed: %v", err)
		}
	}()
	klog.V(2).Info("admission manager started")
	<-stopCh
	return nil
}

// generateCerts generates certificate and privateKey and ca certificate for admission webhook
// server, and they will be saved in a secret.
func (am *admissionManager) generateCerts(create bool) (
	serverCertificate, serverPrivateKey, caCertificate []byte,
	err error) {
	var caPrivateKey []byte
	caPrivateKey, err = utils.SetUpCaKey()
	if err != nil {
		klog.Errorf("set up ca key failed %v", err)
		return nil, nil, nil, err
	}
	caCertificate, err = utils.SetUpCaCert(webhookconstants.ComponentName, caPrivateKey)
	if err != nil {
		klog.Errorf("set up ca cert failed %v", err)
		return nil, nil, nil, err
	}
	namespace := utils.GetCurrentNamespace()
	domains, ips := subjectAltNames(namespace, am.externalService)
	serverCertificate, serverPrivateKey, err = utils.SetUpSignedCertAndKey(domains, ips,
		webhookconstants.ComponentName,
		caPrivateKey, caCertificate, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})
	if err != nil {
		klog.Errorf("set up server cert error %v", err)
		return nil, nil, nil, err
	}
	if create {
		// try to create a new secret to save certificate and privateKey and ca certificate.
		_, err = am.kubeClient.CoreV1().Secrets(namespace).Create(context.Background(),
			&corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      certsSecretName,
					Namespace: namespace,
				},
				Data: map[string][]byte{
					serverCert: serverCertificate,
					serverKey:  serverPrivateKey,
					caCert:     caCertificate,
				},
			}, metav1.CreateOptions{})
		if err != nil {
			klog.Errorf("create new certificate secret error %v", err)
			return nil, nil, nil, err
		}
	} else {
		// try to update an old secret to save certificate and privateKey and ca certificate.
		if err = utils.UpdateSecret(am.kubeClient, namespace, certsSecretName,
			func(secret *corev1.Secret) {
				secret.Data = map[string][]byte{
					serverCert: serverCertificate,
					serverKey:  serverPrivateKey,
					caCert:     caCertificate,
				}
			}); err != nil {
			return nil, nil, nil, err
		}
	}
	return caCertificate, serverCertificate, serverPrivateKey, nil
}

// loadCertsFromSecret loads certificate and privateKey and ca certificate from the secret.
func (am *admissionManager) loadCertsFromSecret() {
	namespace := utils.GetCurrentNamespace()
	create := false
	secret, err := am.kubeClient.CoreV1().Secrets(namespace).Get(context.Background(),
		certsSecretName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			klog.Fatalf("get secret of %v/%v failed: %v", namespace, certsSecretName, err)
		}
		create = true
	}
	var serverCertBody, serverKeyBody, caCertBody []byte
	if secret == nil || secret.Data == nil || len(secret.Data[serverCert]) == 0 || len(secret.Data[serverKey]) == 0 ||
		len(secret.Data[caCert]) == 0 {
		caCertBody, serverCertBody, serverKeyBody, err = am.generateCerts(create)
		if err != nil {
			klog.Fatalf("generate certs failed: %v", err)
		}
	} else {
		caCertBody = secret.Data[caCert]
		serverCertBody = secret.Data[serverCert]
		serverKeyBody = secret.Data[serverKey]
	}
	am.caCertBody = caCertBody
	cert, err := tls.X509KeyPair(serverCertBody, serverKeyBody)
	if err != nil {
		klog.Fatalf("generate key pair error :%v", err)
	}
	am.tlsConfig = &tls.Config{
		Certificates: []tls.Certificate{cert},
	}
}

// subjectAltNames builds subject alt names by namespace and service name.
func subjectAltNames(namespace, svcName string) ([]string, []net.IP) {
	return []string{
		"localhost",
		svcName,
		fmt.Sprintf("%v.%v.svc", svcName, namespace),
		fmt.Sprintf("%v.%v.svc.cluster.local", svcName, namespace),
	}, []net.IP{net.ParseIP("127.0.0.1")}
}
