package controllers

import (
	"context"

	anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1"
	pkg_aws "github.com/aws/aws-application-networking-k8s/pkg/aws"
	"github.com/aws/aws-application-networking-k8s/pkg/aws/services"
	deploy "github.com/aws/aws-application-networking-k8s/pkg/deploy/lattice"
	"github.com/aws/aws-application-networking-k8s/pkg/k8s"
	policy "github.com/aws/aws-application-networking-k8s/pkg/k8s/policyhelper"
	model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice"
	"github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog"

	ctrl "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/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

const (
	IAMAuthPolicyAnnotation      = "iam-auth-policy"
	IAMAuthPolicyAnnotationResId = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation + "-resource-id"
	IAMAuthPolicyAnnotationType  = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation + "-resource-type"
	IAMAuthPolicyFinalizer       = k8s.AnnotationPrefix + IAMAuthPolicyAnnotation
)

type (
	IAP = anv1alpha1.IAMAuthPolicy
)

type IAMAuthPolicyController struct {
	log    gwlog.Logger
	client client.Client
	pm     *deploy.IAMAuthPolicyManager
	ph     *policy.PolicyHandler[*IAP]
	cloud  pkg_aws.Cloud
}

func RegisterIAMAuthPolicyController(log gwlog.Logger, mgr ctrl.Manager, cloud pkg_aws.Cloud) error {
	ph := policy.NewIAMAuthPolicyHandler(log, mgr.GetClient())

	controller := &IAMAuthPolicyController{
		log:    log,
		client: mgr.GetClient(),
		pm:     deploy.NewIAMAuthPolicyManager(cloud),
		ph:     ph,
		cloud:  cloud,
	}

	b := ctrl.
		NewControllerManagedBy(mgr).
		For(&anv1alpha1.IAMAuthPolicy{}, builder.WithPredicates(predicate.GenerationChangedPredicate{}))
	ph.AddWatchers(b, &gwv1.Gateway{}, &gwv1.HTTPRoute{}, &gwv1.GRPCRoute{})
	err := b.Complete(controller)
	return err
}

// Reconciles IAMAuthPolicy CRD.
//
// IAMAuthPolicy has a plain text policy field and targetRef.Content of policy is not validated by
// controller, but Lattice API.
//
// TargetRef Kind can be Gatbeway, HTTPRoute, or GRPCRoute. Other Kinds will result in Invalid
// status.  Policy can be attached to single targetRef only. Attempt to attach more than 1 policy
// will result in Policy Conflict.  If policies created in sequence, the first one will be in
// Accepted status, and second in Conflict.  Any following updates to accepted policy will put it
// into conflicting status, and requires manual resolution - delete conflicting policy.
//
// Lattice side. Gateway attaches to Lattice ServiceNetwork, and HTTP/GRPCRoute to Service.  Policy
// attachment changes ServiceNetowrk and Service auth-type to IAM, and detachment to
// NONE. Successful creation of lattice policy updates k8s policy annotation with ARN/Id of Lattice
// Resouce
//
// Policy Attachment Spec is defined in [GEP-713]: https://gateway-api.sigs.k8s.io/geps/gep-713/.
func (c *IAMAuthPolicyController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	ctx = gwlog.StartReconcileTrace(ctx, c.log, "iamauthpolicy", req.Name, req.Namespace)
	defer func() {
		gwlog.EndReconcileTrace(ctx, c.log)
	}()

	k8sPolicy := &anv1alpha1.IAMAuthPolicy{}
	err := c.client.Get(ctx, req.NamespacedName, k8sPolicy)
	if err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	c.log.Infow(ctx, "reconcile IAM policy", "req", req, "targetRef", k8sPolicy.Spec.TargetRef)
	isDelete := !k8sPolicy.DeletionTimestamp.IsZero()

	var res ctrl.Result
	if isDelete {
		res, err = c.reconcileDelete(ctx, k8sPolicy)
	} else {
		res, err = c.reconcileUpsert(ctx, k8sPolicy)
	}
	if err != nil {
		return ctrl.Result{}, err
	}

	err = c.client.Update(ctx, k8sPolicy)
	if err != nil {
		return reconcile.Result{}, err
	}

	c.log.Infow(ctx, "reconciled IAM policy",
		"req", req,
		"targetRef", k8sPolicy.Spec.TargetRef,
		"isDeleted", isDelete,
	)
	return res, nil
}

func (c *IAMAuthPolicyController) reconcileDelete(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (ctrl.Result, error) {
	err := c.ph.ValidateTargetRef(ctx, k8sPolicy)
	if err == nil {
		modelPolicy := model.NewIAMAuthPolicy(k8sPolicy)
		_, err := c.pm.Delete(ctx, modelPolicy)
		if err != nil {
			return ctrl.Result{}, services.IgnoreNotFound(err)
		}
	}
	err = c.handleLatticeResourceChange(ctx, k8sPolicy, model.IAMAuthPolicyStatus{})
	if err != nil {
		return ctrl.Result{}, err
	}
	c.removeFinalizer(k8sPolicy)
	return ctrl.Result{}, nil
}

func (c *IAMAuthPolicyController) reconcileUpsert(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy) (ctrl.Result, error) {
	reason, err := c.ph.ValidateAndUpdateCondition(ctx, k8sPolicy)
	if err != nil {
		return ctrl.Result{}, err
	}
	if reason != policy.ReasonAccepted {
		return ctrl.Result{}, nil
	}
	modelPolicy := model.NewIAMAuthPolicy(k8sPolicy)
	c.addFinalizer(k8sPolicy)
	err = c.client.Update(ctx, k8sPolicy)
	if err != nil {
		return reconcile.Result{}, err
	}
	statusPolicy, err := c.pm.Put(ctx, modelPolicy)
	if err != nil {
		return reconcile.Result{}, services.IgnoreNotFound(err)
	}
	c.updateLatticeAnnotaion(k8sPolicy, statusPolicy.ResourceId, modelPolicy.Type)
	err = c.handleLatticeResourceChange(ctx, k8sPolicy, statusPolicy)
	if err != nil {
		return reconcile.Result{}, err
	}
	return ctrl.Result{}, nil
}

func (c *IAMAuthPolicyController) removeFinalizer(k8sPolicy *anv1alpha1.IAMAuthPolicy) {
	if controllerutil.ContainsFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) {
		controllerutil.RemoveFinalizer(k8sPolicy, IAMAuthPolicyFinalizer)
	}
}

func (c *IAMAuthPolicyController) addFinalizer(k8sPolicy *anv1alpha1.IAMAuthPolicy) {
	if !controllerutil.ContainsFinalizer(k8sPolicy, IAMAuthPolicyFinalizer) {
		controllerutil.AddFinalizer(k8sPolicy, IAMAuthPolicyFinalizer)
	}
}

// cleanup lattice resources after targetRef changes
func (c *IAMAuthPolicyController) handleLatticeResourceChange(ctx context.Context, k8sPolicy *anv1alpha1.IAMAuthPolicy, statusPolicy model.IAMAuthPolicyStatus) error {
	prevModel, ok := c.getLatticeAnnotation(k8sPolicy)
	if !ok {
		return nil
	}
	if prevModel.ResourceId != statusPolicy.ResourceId {
		_, err := c.pm.Delete(ctx, prevModel)
		if err != nil {
			return services.IgnoreNotFound(err)
		}
	}
	return nil
}

func (c *IAMAuthPolicyController) updateLatticeAnnotaion(k8sPolicy *anv1alpha1.IAMAuthPolicy, resId, resType string) {
	if k8sPolicy.Annotations == nil {
		k8sPolicy.Annotations = make(map[string]string)
	}
	k8sPolicy.Annotations[IAMAuthPolicyAnnotationResId] = resId
	k8sPolicy.Annotations[IAMAuthPolicyAnnotationType] = resType
}

func (c *IAMAuthPolicyController) getLatticeAnnotation(k8sPolicy *anv1alpha1.IAMAuthPolicy) (model.IAMAuthPolicy, bool) {
	if k8sPolicy.Annotations == nil {
		return model.IAMAuthPolicy{}, false
	}
	resourceId := k8sPolicy.Annotations[IAMAuthPolicyAnnotationResId]
	resourceType := k8sPolicy.Annotations[IAMAuthPolicyAnnotationType]
	if resourceId == "" || resourceType == "" {
		return model.IAMAuthPolicy{}, false
	}
	return model.IAMAuthPolicy{
		Type:       resourceType,
		ResourceId: resourceId,
	}, true
}
