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

import (
	"context"
	"errors"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	discoveryv1 "k8s.io/api/discovery/v1"
	networkingv1 "k8s.io/api/networking/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/handler"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	"github.com/apache/apisix-ingress-controller/api/v1alpha1"
	"github.com/apache/apisix-ingress-controller/internal/controller/config"
	"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
	"github.com/apache/apisix-ingress-controller/internal/provider"
	"github.com/apache/apisix-ingress-controller/internal/utils"
	pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
)

// GatewayProxyController reconciles a GatewayProxy object.
type GatewayProxyController struct {
	client.Client

	Scheme   *runtime.Scheme
	Log      logr.Logger
	Provider provider.Provider

	disableGatewayAPI bool
}

func (r *GatewayProxyController) SetupWithManager(mrg ctrl.Manager) error {
	hasGatewayAPI := false
	if !config.ControllerConfig.DisableGatewayAPI {
		var err error
		if hasGatewayAPI, err = pkgutils.HasAPIResource(mrg, &gatewayv1.Gateway{}); err != nil {
			return err
		}
	}
	r.disableGatewayAPI = !hasGatewayAPI
	builder := ctrl.NewControllerManagedBy(mrg).
		For(&v1alpha1.GatewayProxy{}).
		WithEventFilter(
			predicate.Or(
				predicate.GenerationChangedPredicate{},
				predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
			),
		).
		Watches(&corev1.Service{},
			handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForProviderService),
		).
		Watches(&discoveryv1.EndpointSlice{},
			handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForProviderEndpointSlice),
		).
		Watches(&corev1.Secret{},
			handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForSecret),
		).
		Watches(&networkingv1.IngressClass{},
			handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForIngressClass),
		)
	if !r.disableGatewayAPI {
		builder.Watches(&gatewayv1.Gateway{},
			handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesByGateway),
		)
	}
	return builder.Complete(r)
}

func (r *GatewayProxyController) Reconcile(ctx context.Context, req ctrl.Request) (reconcile.Result, error) {
	var tctx = provider.NewDefaultTranslateContext(ctx)

	var gp v1alpha1.GatewayProxy
	if err := r.Get(ctx, req.NamespacedName, &gp); err != nil {
		if client.IgnoreNotFound(err) == nil {
			gp.Namespace = req.Namespace
			gp.Name = req.Name
			err = r.Provider.Update(ctx, tctx, &gp)
		}
		return ctrl.Result{}, err
	}

	// if there is no provider, update with empty translate context
	if gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
		return reconcile.Result{}, r.Provider.Update(ctx, tctx, &gp)
	}

	// process endpoints for provider service
	providerService := gp.Spec.Provider.ControlPlane.Service
	if providerService == nil {
		tctx.EndpointSlices[req.NamespacedName] = nil
	} else {
		if err := addProviderEndpointsToTranslateContext(tctx, r.Client, r.Log, types.NamespacedName{
			Namespace: gp.Namespace,
			Name:      providerService.Name,
		}); err != nil {
			return reconcile.Result{}, err
		}
	}

	// process secret for provider auth
	auth := gp.Spec.Provider.ControlPlane.Auth
	if auth.AdminKey != nil && auth.AdminKey.ValueFrom != nil && auth.AdminKey.ValueFrom.SecretKeyRef != nil {
		var (
			secret   corev1.Secret
			secretNN = types.NamespacedName{
				Namespace: gp.GetNamespace(),
				Name:      auth.AdminKey.ValueFrom.SecretKeyRef.Name,
			}
		)
		if err := r.Get(ctx, secretNN, &secret); err != nil {
			r.Log.Error(err, "failed to get secret", "secret", secretNN)
			return reconcile.Result{}, err
		}
		tctx.Secrets[secretNN] = &secret
	}

	// list Gateways that reference the GatewayProxy
	var (
		gatewayList      gatewayv1.GatewayList
		ingressClassList networkingv1.IngressClassList
		indexKey         = indexer.GenIndexKey(gp.GetNamespace(), gp.GetName())
	)

	if !r.disableGatewayAPI {
		if err := r.List(ctx, &gatewayList, client.MatchingFields{indexer.ParametersRef: indexKey}); err != nil {
			r.Log.Error(err, "failed to list GatewayList")
			return ctrl.Result{}, nil
		}
		var gatewayclassList gatewayv1.GatewayClassList
		if err := r.List(ctx, &gatewayclassList, client.MatchingFields{indexer.ControllerName: config.GetControllerName()}); err != nil {
			r.Log.Error(err, "failed to list GatewayClassList")
			return ctrl.Result{}, nil
		}
		gcMatched := make(map[string]*gatewayv1.GatewayClass)
		for _, item := range gatewayclassList.Items {
			gcMatched[item.Name] = &item
		}
		// append referrers to translate context
		for _, item := range gatewayList.Items {
			gcName := string(item.Spec.GatewayClassName)
			if gcName == "" {
				continue
			}
			if _, ok := gcMatched[gcName]; ok {
				tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item))
			}
		}
		r.Log.V(1).Info("found Gateways for GatewayProxy", "gatewayproxy", req.String(), "gateways", len(gatewayList.Items), "gatewayclasses", len(gatewayclassList.Items), "ingressclasses", len(ingressClassList.Items))
	}

	// list IngressClasses that reference the GatewayProxy
	if err := r.List(ctx, &ingressClassList, client.MatchingFields{indexer.IngressClassParametersRef: indexKey}); err != nil {
		r.Log.Error(err, "failed to list IngressClassList")
		return reconcile.Result{}, err
	}

	for _, item := range ingressClassList.Items {
		if item.Spec.Controller != config.GetControllerName() {
			continue
		}
		tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item))
	}

	if len(tctx.GatewayProxyReferrers[req.NamespacedName]) == 0 {
		return ctrl.Result{}, nil
	}

	r.Log.V(1).Info("references found for GatewayProxy", "gatewayproxy", req.String(), "references", tctx.GatewayProxyReferrers[req.NamespacedName])
	if err := r.Provider.Update(ctx, tctx, &gp); err != nil {
		return reconcile.Result{}, err
	}

	return reconcile.Result{}, nil
}

func (r *GatewayProxyController) listGatewayProxiesForProviderService(ctx context.Context, obj client.Object) (requests []reconcile.Request) {
	service, ok := obj.(*corev1.Service)
	if !ok {
		r.Log.Error(errors.New("unexpected object type"), "failed to convert object to Service")
		return nil
	}

	return ListRequests(ctx, r.Client, r.Log, &v1alpha1.GatewayProxyList{}, client.MatchingFields{
		indexer.ServiceIndexRef: indexer.GenIndexKey(service.GetNamespace(), service.GetName()),
	})
}

func (r *GatewayProxyController) listGatewayProxiesForProviderEndpointSlice(ctx context.Context, obj client.Object) (requests []reconcile.Request) {
	endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
	if !ok {
		r.Log.Error(errors.New("unexpected object type"), "failed to convert object to EndpointSlice")
		return nil
	}

	return ListRequests(ctx, r.Client, r.Log, &v1alpha1.GatewayProxyList{}, client.MatchingFields{
		indexer.ServiceIndexRef: indexer.GenIndexKey(endpointSlice.GetNamespace(), endpointSlice.Labels[discoveryv1.LabelServiceName]),
	})
}

func (r *GatewayProxyController) listGatewayProxiesForSecret(ctx context.Context, object client.Object) []reconcile.Request {
	secret, ok := object.(*corev1.Secret)
	if !ok {
		r.Log.Error(errors.New("unexpected object type"), "failed to convert object to Secret")
		return nil
	}
	return ListRequests(ctx, r.Client, r.Log, &v1alpha1.GatewayProxyList{}, client.MatchingFields{
		indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()),
	})
}

func (r *GatewayProxyController) listGatewayProxiesForIngressClass(ctx context.Context, object client.Object) []reconcile.Request {
	ingressClass, ok := object.(*networkingv1.IngressClass)
	if !ok {
		r.Log.Error(errors.New("unexpected object type"), "failed to convert object to IngressClass")
		return nil
	}
	reqs := []reconcile.Request{}
	gp, _ := GetGatewayProxyByIngressClass(ctx, r.Client, ingressClass)
	if gp != nil {
		reqs = append(reqs, reconcile.Request{
			NamespacedName: utils.NamespacedName(gp),
		})
	}
	return reqs
}

func (r *GatewayProxyController) listGatewayProxiesByGateway(ctx context.Context, object client.Object) []reconcile.Request {
	gateway, ok := object.(*gatewayv1.Gateway)
	if !ok {
		r.Log.Error(errors.New("unexpected object type"), "failed to convert object to IngressClass")
		return nil
	}
	reqs := []reconcile.Request{}
	gp, _ := GetGatewayProxyByGateway(ctx, r.Client, gateway)
	if gp != nil {
		reqs = append(reqs, reconcile.Request{
			NamespacedName: utils.NamespacedName(gp),
		})
	}
	return reqs
}
