/*
Copyright 2023.

Licensed 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 controller

import (
	"context"
	"errors"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/aws/aws-sdk-go-v2/service/acm"
	customdomainv1 "github.com/tidbcloud/api-gateway/operator/api/v1"
	"github.com/tidbcloud/api-gateway/operator/internal/util"
)

const (
	deployFinalizerName = "deploy.apigateway.tidbcloud.com/finalizer"
)

// CustomDomainDeploymentReconciler reconciles a CustomDomainDeployment object
type CustomDomainDeploymentReconciler struct {
	client.Client
	Scheme              *runtime.Scheme
	AcmClient           *acm.Client
	AlbIngressName      string
	AlbIngressNamespace string
}

//+kubebuilder:rbac:groups=customdomain.apigateway.tidbcloud.com,resources=customdomaindeployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=customdomain.apigateway.tidbcloud.com,resources=customdomaindeployments/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=customdomain.apigateway.tidbcloud.com,resources=customdomaindeployments/finalizers,verbs=update
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the CustomDomainDeployment object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile
func (r *CustomDomainDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx)
	log.Info("CustomDomainDeploymentReconciler reconcile started")

	var deploy customdomainv1.CustomDomainDeployment
	if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil {
		log.Error(err, "unable to fetch CustomDomainDeployment")
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	if !util.CustomDomainDeploymentHasCondition(&deploy, customdomainv1.CustomDomainDeploymentCondition{
		Type:   customdomainv1.CustomDomainDeploymentConditionCertImported,
		Status: customdomainv1.ConditionTrue,
	}) {
		var secret corev1.Secret
		if err := r.Get(ctx, types.NamespacedName{Name: deploy.Spec.SecretName, Namespace: req.Namespace}, &secret); err != nil {
			log.Error(err, "failed to get deploy related secret", "host", deploy.Spec.Host)
			return ctrl.Result{}, err
		}

		log.Info("importing certificate to aws", "host", deploy.Spec.Host)
		certArn, err := r.importCertificateToAWS(ctx, &secret)
		if err != nil {
			log.Error(err, "import certificate to aws failed, retry after 10 seconds", "host", deploy.Spec.Host)
			// avoid frequently request aws
			return ctrl.Result{RequeueAfter: time.Second * 10}, nil
		}
		if err := r.updateCertificateArn(ctx, &deploy, certArn); err != nil {
			log.Error(err, "failed to update deploy status certArn", "host", deploy.Spec.Host)
			return ctrl.Result{}, err
		}
	}

	var childAlbs customdomainv1.CustomDomainAlbList
	if err := r.List(ctx, &childAlbs, client.InNamespace(req.Namespace), client.MatchingFields{resouceOwnerKey: req.Name}); err != nil {
		log.Error(err, "unable to list child customDomainAlbs", "host", deploy.Spec.Host)
		return ctrl.Result{}, err
	}
	// Ensure resource will be cleared with finalizer
	if deploy.DeletionTimestamp.IsZero() {
		if !controllerutil.ContainsFinalizer(&deploy, deployFinalizerName) {
			controllerutil.AddFinalizer(&deploy, deployFinalizerName)
			if err := r.Update(ctx, &deploy); err != nil {
				log.Error(err, "failed to add finalizer", "host", deploy.Spec.Host)
				return ctrl.Result{}, err
			}
		}
	} else {
		if controllerutil.ContainsFinalizer(&deploy, deployFinalizerName) {
			if len(childAlbs.Items) > 0 {
				log.Info("updating certificate suspend", "host", deploy.Spec.Host)
				alb := childAlbs.Items[0]
				alb.Spec.Suspend = true
				if err := r.Update(ctx, &alb); err != nil {
					log.Error(err, "failed to update alb suspend", "host", deploy.Spec.Host)
					return ctrl.Result{}, err
				}
			}
			log.Info("removing certificate from aws", "host", deploy.Spec.Host)
			if err := r.clearCertificateFromAWS(ctx, &deploy); err != nil {
				log.Error(err, "remove certificate from aws failed, retry after 10 seconds", "host", deploy.Spec.Host)
				return ctrl.Result{RequeueAfter: time.Second * 10}, nil
			}
			controllerutil.RemoveFinalizer(&deploy, deployFinalizerName)
			if err := r.Update(ctx, &deploy); err != nil {
				log.Error(err, "failed to remove finalizer", "host", deploy.Spec.Host)
				return ctrl.Result{}, err
			}
		}
		log.Info("customdomaindeployment deletion success", "host", deploy.Spec.Host)
		return ctrl.Result{}, nil
	}

	if len(childAlbs.Items) == 0 {
		alb, err := r.constructAlbForCustomDomainDeployment(&deploy)
		if err != nil {
			log.Error(err, "unable to construct alb for CustomDomainDeployment", "host", deploy.Spec.Host)
			return ctrl.Result{}, err
		}
		if err := r.Create(ctx, alb); err != nil {
			log.Error(err, "unable to create alb for CustomDomainDeployment", "host", deploy.Spec.Host)
			return ctrl.Result{}, err
		}
	} else {
		// a CustomDomainDeployment only contains only one CustomDomainAlb
		alb := childAlbs.Items[0]
		if util.CustomDomainAlbHasCondition(&alb, customdomainv1.CustomDomainAlbCondition{
			Type:   customdomainv1.CustomDomainAlbConditionReady,
			Status: customdomainv1.ConditionTrue,
		}) {
			if err := r.setDeployReady(ctx, &deploy, alb.Status.Revision); err != nil {
				log.Error(err, "set customdomain deployment ready failed", "host", deploy.Spec.Host)
				return ctrl.Result{}, err
			}
		} else if util.CustomDomainAlbHasCondition(&alb, customdomainv1.CustomDomainAlbCondition{
			Type:   customdomainv1.CustomDomainAlbConditionRetired,
			Status: customdomainv1.ConditionTrue,
		}) {
			if err := r.setDeployRetired(ctx, &deploy); err != nil {
				log.Error(err, "set customdomain deployment retired failed", "host", deploy.Spec.Host)
				return ctrl.Result{}, err
			}
		}
	}

	log.Info("CustomDomainDeploymentReconciler reconcile finished", "host", deploy.Spec.Host)
	return ctrl.Result{}, nil
}

func (r *CustomDomainDeploymentReconciler) constructAlbForCustomDomainDeployment(deploy *customdomainv1.CustomDomainDeployment) (*customdomainv1.CustomDomainAlb, error) {
	alb := customdomainv1.CustomDomainAlb{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: deploy.Namespace,
			Name:      deploy.Name,
		},
		Spec: customdomainv1.CustomDomainAlbSpec{
			IngressName:      r.AlbIngressName,
			IngressNamespace: r.AlbIngressNamespace,
			Suspend:          false,
			DynamicConfig: &customdomainv1.CustomDomainAlbDynamicConfig{
				Host:                deploy.Spec.Host,
				CertificateRevision: deploy.Spec.CertificateRevision,
				DynamicCertArn:      deploy.Status.CertificateArn,
			},
		},
	}
	if err := ctrl.SetControllerReference(deploy, &alb, r.Scheme); err != nil {
		return nil, err
	}
	return &alb, nil
}

func (r *CustomDomainDeploymentReconciler) clearCertificateFromAWS(ctx context.Context, deploy *customdomainv1.CustomDomainDeployment) error {
	log := log.FromContext(ctx)
	_, err := r.AcmClient.GetCertificate(ctx, &acm.GetCertificateInput{CertificateArn: &deploy.Status.CertificateArn})
	if err != nil {
		log.Error(err, "can't get certificate, it may has already been deleted, skip clearing", "host", deploy.Spec.Host)
		return nil
	}
	_, err = r.AcmClient.DeleteCertificate(ctx, &acm.DeleteCertificateInput{CertificateArn: &deploy.Status.CertificateArn})
	if err != nil {
		return err
	}
	return nil
}

func (r *CustomDomainDeploymentReconciler) importCertificateToAWS(ctx context.Context, secret *corev1.Secret) (string, error) {
	var certificateChain []byte
	var privateKey []byte
	for k, v := range secret.Data {
		if k == "tls.crt" {
			certificateChain = v
		} else if k == "tls.key" {
			privateKey = v
		}
	}
	sep := "-----END CERTIFICATE-----"
	parts := strings.Split(string(certificateChain), sep)
	if len(parts) == 0 {
		return "", errors.New("bad certificate chain")
	}
	certificate := []byte(parts[0] + sep)
	output, err := r.AcmClient.ImportCertificate(ctx, &acm.ImportCertificateInput{
		Certificate:      certificate,
		PrivateKey:       privateKey,
		CertificateChain: certificateChain,
	})
	if err != nil {
		return "", err
	}
	return *output.CertificateArn, nil
}

func (r *CustomDomainDeploymentReconciler) updateCertificateArn(
	ctx context.Context,
	deploy *customdomainv1.CustomDomainDeployment,
	certArn string,
) error {
	deploy.Status.CertificateArn = certArn
	util.SetCustomDomainDeploymentCondition(
		ctx,
		deploy,
		customdomainv1.CustomDomainDeploymentConditionCertImported,
		customdomainv1.ConditionTrue,
	)
	return r.Status().Update(ctx, deploy)
}

func (r *CustomDomainDeploymentReconciler) setDeployReady(
	ctx context.Context,
	deploy *customdomainv1.CustomDomainDeployment,
	albRevision int,
) error {
	log := log.FromContext(ctx)
	log.Info("customdomaindeployment has been ready", "albRevision", albRevision, "host", deploy.Spec.Host)
	deploy.Status.AlbRevision = albRevision
	util.SetCustomDomainDeploymentCondition(
		ctx,
		deploy,
		customdomainv1.CustomDomainDeploymentConditionReady,
		customdomainv1.ConditionTrue,
	)
	return r.Status().Update(ctx, deploy)
}

func (r *CustomDomainDeploymentReconciler) setDeployRetired(ctx context.Context, deploy *customdomainv1.CustomDomainDeployment) error {
	log := log.FromContext(ctx)
	log.Info("customdomaindeployment has been retired", "host", deploy.Spec.Host)
	util.RemoveCustomDomainDeploymentCondition(deploy, customdomainv1.CustomDomainDeploymentConditionReady)
	util.SetCustomDomainDeploymentCondition(
		ctx,
		deploy,
		customdomainv1.CustomDomainDeploymentConditionRetired,
		customdomainv1.ConditionTrue,
	)
	return r.Status().Update(ctx, deploy)
}

// SetupWithManager sets up the controller with the Manager.
func (r *CustomDomainDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &customdomainv1.CustomDomainAlb{}, resouceOwnerKey, func(rawObj client.Object) []string {
		// grab the ingress object, extract the owner...
		alb := rawObj.(*customdomainv1.CustomDomainAlb)
		owner := metav1.GetControllerOf(alb)
		if owner == nil {
			return nil
		}
		// ...make sure it's a CustomDomain...
		if owner.APIVersion != apiGVStr || owner.Kind != "CustomDomainDeployment" {
			return nil
		}
		// ...and if so, return it
		return []string{owner.Name}
	}); err != nil {
		return err
	}
	return ctrl.NewControllerManagedBy(mgr).
		For(&customdomainv1.CustomDomainDeployment{}).
		Owns(&customdomainv1.CustomDomainAlb{}).
		Complete(r)
}
