/*
Copyright 2021.

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 controllers

import (
	"context"
	"fmt"
	"reflect"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"

	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	lambdav1alpha1 "github.com/crossplane/provider-aws/apis/lambda/v1alpha1"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/pkg/k8s"
	"github.com/tidbcloud/eks-provider/pkg/procedure"
)

const (
	// EKSConnectionFinalizer is the finalizer name for EKSConnection.
	EKSConnectionFinalizer = "eksconnections.infra.pingcap.com/finalizer"
)

// EKSConnectionReconciler reconciles a EKSConnection object
type EKSConnectionReconciler struct {
	client.Client
	BaseCli   client.Client
	Scheme    *runtime.Scheme
	Logger    logr.Logger
	Namespace string
	TokenGen  token.Generator
	AWSSVC    procedure.AWSService
}

//+kubebuilder:rbac:groups=eks.infra.pingcap.com,resources=eksconnections,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=eks.infra.pingcap.com,resources=eksconnections/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=eks.infra.pingcap.com,resources=eksconnections/finalizers,verbs=update
//+kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=list;watch;get;patch;create;update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;patch;create;update
//+kubebuilder:rbac:groups=lambda.aws.crossplane.io,resources=functions,verbs=list;watch;get;patch;create;update
//+kubebuilder:rbac:groups=ec2.aws.crossplane.io,resources=vpcendpointserviceconfigurations;vpcendpoints,verbs=list;watch;get;patch;create;update

// Reconcile drives the EKS object to the desired state.
func (r *EKSConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
	log := r.Logger.WithValues("object", req.NamespacedName)

	conn := &eksv1alpha1.EKSConnection{}
	err = r.Get(ctx, req.NamespacedName, conn)
	if err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}
	originalConn := conn.DeepCopy()
	// update object if necessary
	defer func() {
		if err != nil {
			return
		}
		if !reflect.DeepEqual(&conn.Status, &originalConn.Status) {
			err = r.Status().Update(ctx, conn)
			if err != nil {
				res = ctrl.Result{}
				return
			}
		}
		// ignore status changes and check remaining parts
		conn.Status, originalConn.Status = eksv1alpha1.EKSConnectionStatus{}, eksv1alpha1.EKSConnectionStatus{}
		if !reflect.DeepEqual(conn, originalConn) {
			err = r.Update(ctx, conn)
			if err != nil {
				res = ctrl.Result{}
				return
			}
		}
	}()

	if !conn.DeletionTimestamp.IsZero() {
		return r.reconcileDelete(ctx, log, conn)
	}
	return r.reconcileNormal(ctx, log, conn)
}

// reconcileDelete remove EKSConnection related resources.
func (r *EKSConnectionReconciler) reconcileDelete(ctx context.Context, log logr.Logger, conn *eksv1alpha1.EKSConnection) (ctrl.Result, error) {
	info, err := procedure.GetProcedureInfo(ctx, r.Client, conn, r.Namespace)
	if err != nil {
		return ctrl.Result{}, err
	}
	err = procedure.NewFrpsSVC(nil).Cleanup(ctx, r.BaseCli, r.Client, info)
	if err != nil {
		return ctrl.Result{}, err
	}
	err = procedure.NewFrpsDeploy().Cleanup(ctx, r.BaseCli, r.Client, info)
	if err != nil {
		return ctrl.Result{}, err
	}
	err = procedure.NewCoreDNSCM().Cleanup(ctx, r.BaseCli, r.Client, info)
	if err != nil {
		return ctrl.Result{}, err
	}
	err = procedure.NewFrpsCM().Cleanup(ctx, r.BaseCli, r.Client, info)
	if err != nil {
		return ctrl.Result{}, err
	}
	controllerutil.RemoveFinalizer(conn, EKSConnectionFinalizer)
	return ctrl.Result{}, nil
}

// reconcileNormal ensures resources are provisioned for the connection.
func (r *EKSConnectionReconciler) reconcileNormal(ctx context.Context, log logr.Logger, conn *eksv1alpha1.EKSConnection) (ctrl.Result, error) {
	// Set Ready to False by default
	defer func() {
		controllerutil.AddFinalizer(conn, EKSConnectionFinalizer)
		if conn.Status.Conditions.GetCondition(eksv1alpha1.ConditionReady).Status == corev1.ConditionUnknown {
			conn.Status.Conditions.SetCondition(eksv1alpha1.Condition{
				Type:               eksv1alpha1.ConditionReady,
				Status:             corev1.ConditionFalse,
				LastTransitionTime: metav1.Now(),
			})
		}
	}()
	var (
		// frpEpSvcProcedures provision endpoint service for frps
		frpEpSvcProcedures = []procedure.Procedure{
			procedure.NewFrpsEpSvc(procedure.NewFrpsPrivatelinkHelper(r.AWSSVC)),
		}
		// frpEpSvcProcedures provision endpoint on tenant side
		frpEpProcedures = []procedure.Procedure{
			procedure.NewFrpsEp(procedure.NewPlMngr(r.AWSSVC)),
		}
		// frpProcedures provision frp objects
		frpProcedures = []procedure.Procedure{
			procedure.NewLambdaFunc(),
			procedure.NewFrpsCM(),
			procedure.NewFrpsDeploy(),
			procedure.NewFrpsSVC(procedure.NewFrpsPrivatelinkHelper(r.AWSSVC)),
		}
		// corednsProcedures modifies the coredns record
		corednsProcedures = []procedure.Procedure{
			procedure.NewCoreDNSCM(),
		}
		// remoteProcedures execute procedures on the remote EKS cluster
		// TODO: send arbitrary request to EKS apiserver to test network connectivity
		remoteProcedures = []procedure.Procedure{
			procedure.InvokeLambda(k8s.NewEKSTokenManager(r.Client, r.TokenGen), r.AWSSVC, procedure.NewPlMngr(r.AWSSVC)),
		}
	)

	info, err := procedure.GetProcedureInfo(ctx, r.Client, conn, r.Namespace)
	if err != nil {
		log.Error(err, "failed to retrieve procedure info")
		conn.Status.Conditions.SetCondition(eksv1alpha1.Condition{
			Type:    eksv1alpha1.ConditionReady,
			Status:  corev1.ConditionFalse,
			Reason:  err.Error(),
			Message: "failed to retrieve procedure info",
		})
		return ctrl.Result{}, err
	}

	// stateTrans specifies the conditions for resource provision and observation respectively.
	// it also specifies the group of procedures required during state transition.
	type stateTrans struct {
		// procedures specifies a group of procedures that can be executed without dependencies.
		procedures []procedure.Procedure
		// deployType specifies the condition type for resource creation.
		deployType eksv1alpha1.ConditionType
		// statusType specifies the condition type for polling resource status.
		statusType eksv1alpha1.ConditionType
	}

	// stateTransResult specifies the result of state transition.
	type stateTransResult struct {
		// err specifies the error that occurred during the state transition.
		err error
		// ready specifies whether the state transition completes.
		ready bool
		// deployCond specifies the condition for resource creation.
		deployCond eksv1alpha1.Condition
		// statusCond specifies the condition for polling resource status.
		statusCond eksv1alpha1.Condition
	}
	// states specifies the ordered state transition list.
	states := []stateTrans{
		{
			deployType: eksv1alpha1.FrpObjectsCreated,
			statusType: eksv1alpha1.FrpObjectsReady,
			procedures: frpProcedures,
		},
		{
			deployType: eksv1alpha1.FrpsEndpointSvcCreated,
			statusType: eksv1alpha1.FrpsEndpointSvcReady,
			procedures: frpEpSvcProcedures,
		},
		{
			deployType: eksv1alpha1.FrpsEndpointCreated,
			statusType: eksv1alpha1.FrpsEndpointReady,
			procedures: frpEpProcedures,
		},
		{
			deployType: eksv1alpha1.LocalObjectsCreated,
			statusType: eksv1alpha1.LocalObjectsReady,
			procedures: corednsProcedures,
		},
		{
			deployType: eksv1alpha1.RemoteObjectsCreated,
			statusType: eksv1alpha1.ConditionReady,
			procedures: remoteProcedures,
		},
	}

	updateState := func(trans stateTrans) (res stateTransResult) {
		// first execute the procedures
		res.deployCond.Type = trans.deployType
		res.deployCond.LastTransitionTime = metav1.Now()
		name, err := executeProcedures(ctx, r.BaseCli, r.Client, info, trans.procedures)
		if err != nil {
			res.err = fmt.Errorf("procedure %s failed: %v", name, err)
			res.deployCond.Status = corev1.ConditionFalse
			res.deployCond.Reason = err.Error()
			res.deployCond.Message = fmt.Sprintf("execute procedure %s failed", name)
			return
		}
		res.deployCond.Status = corev1.ConditionTrue

		// then poll the procedure status
		res.statusCond.Type = trans.statusType
		res.statusCond.LastTransitionTime = metav1.Now()
		name, err = getProceduresStatus(ctx, r.BaseCli, r.Client, info, trans.procedures)
		if name != "" {
			if err == nil {
				res.statusCond.Message = fmt.Sprintf("poll procedure %s status failed", name)
			} else {
				res.statusCond.Message = fmt.Sprintf("procedure %s not ready", name)
			}
			res.statusCond.Status = corev1.ConditionFalse
			if err != nil {
				res.statusCond.Reason = err.Error()
				res.err = fmt.Errorf("get procedure %s status failed: %v", name, err)
				return
			}
			return
		}
		res.statusCond.Status = corev1.ConditionTrue
		res.ready = true
		return
	}

	for _, state := range states {
		res := updateState(state)
		if !procedure.ConditionEqual(conn.Status.Conditions.GetCondition(state.deployType), res.deployCond) {
			conn.Status.Conditions.SetCondition(res.deployCond)
		}
		if !procedure.ConditionEqual(conn.Status.Conditions.GetCondition(state.statusType), res.statusCond) {
			conn.Status.Conditions.SetCondition(res.statusCond)
		}

		if res.err != nil {
			return ctrl.Result{}, res.err
		}
		if res.ready {
			continue
		}
		return ctrl.Result{Requeue: true}, nil
	}

	return ctrl.Result{}, nil
}

// executeProcedures create k8s objects in PingCAP's k8s.
// if failed, return the failing procedure name and the error.
func executeProcedures(ctx context.Context, base, infra client.Client, info procedure.ProcedureInfo, lps []procedure.Procedure) (string, error) {
	for _, lp := range lps {
		if err := lp.Deploy(ctx, base, infra, info); err != nil {
			return lp.Name(), err
		}
	}
	return "", nil
}

// getProceduresStatus create k8s objects in PingCAP's k8s.
// if failed, return the failing procedure name and the error.
func getProceduresStatus(ctx context.Context, base, infra client.Client, info procedure.ProcedureInfo, lps []procedure.Procedure) (string, error) {
	for _, lp := range lps {
		ready, err := lp.Status(ctx, base, infra, info)
		if err != nil {
			return lp.Name(), err
		}
		if !ready {
			return lp.Name(), nil
		}
	}
	return "", nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *EKSConnectionReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&eksv1alpha1.EKSConnection{}).
		Owns(&ec2v1alpha1.VPCEndpoint{}).
		Owns(&ec2v1alpha1.VPCEndpointServiceConfiguration{}).
		Owns(&lambdav1alpha1.Function{}).
		Complete(r)
}
