// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package gateway_api

import (
	"context"
	"log/slog"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

	"github.com/cilium/cilium/operator/pkg/gateway-api/helpers"
	"github.com/cilium/cilium/pkg/logging/logfields"
)

const (
	// controllerName is the gateway controller name used in cilium.
	controllerName = "io.cilium/gateway-controller"

	// Indexes HTTPRoutes by all the backend Services referenced in the object.
	backendServiceHTTPRouteIndex = "backendServiceHTTPRouteIndex"

	// Indexes HTTPRoutes by all the backend ServiceImports referenced in the object.
	backendServiceImportHTTPRouteIndex = "backendServiceImportHTTPRouteIndex"

	// Indexes HTTPRoutes by all the Gateway parents referenced in the object.
	gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex"

	// Indexes Gateways and records if the Gateway is relevant for Cilium.
	implementationGatewayIndex = "implementationGatewayIndex"

	// Indexes TLSRoutes by all the backend Services referenced in the object.
	backendServiceTLSRouteIndex = "backendServiceTLSRouteIndex"

	// Indexes TLSRoutes by all the Gateway parents referenced in the object.
	gatewayTLSRouteIndex = "gatewayTLSRouteIndex"

	// Indexes GRPCRoutes by all the backend Services referenced in the object.
	backendServiceGRPCRouteIndex = "backendServiceGRPCRouteIndex"

	// Indexes GRPCRoutes by all the Gateway parents referenced in the object.
	gatewayGRPCRouteIndex = "gatewayGRPCRouteIndex"

	// Indexes GAMMA HTTPRoutes by all the GAMMA parents of that HTTPRoute.
	// This is then be used by the Service reconciler to only retrieve any HTTPRoutes that have that specific
	// Service as a parent.
	gammaParentRefsIndex = "gammaParentRefs"
)

func hasMatchingController(ctx context.Context, c client.Client, controllerName string, logger *slog.Logger) func(object client.Object) bool {
	return func(obj client.Object) bool {
		scopedLog := logger.With(
			logfields.Resource, obj.GetName(),
		)
		gw, ok := obj.(*gatewayv1.Gateway)
		if !ok {
			return false
		}

		gwc := &gatewayv1.GatewayClass{}
		key := types.NamespacedName{Name: string(gw.Spec.GatewayClassName)}
		if err := c.Get(ctx, key, gwc); err != nil {
			scopedLog.ErrorContext(ctx, "Unable to get GatewayClass", logfields.Error, err)
			return false
		}

		return string(gwc.Spec.ControllerName) == controllerName
	}
}

func getGatewaysForSecret(ctx context.Context, c client.Client, obj client.Object, logger *slog.Logger) []*gatewayv1.Gateway {
	scopedLog := logger.With(
		logfields.Resource, obj.GetName(),
	)

	gwList := &gatewayv1.GatewayList{}
	if err := c.List(ctx, gwList); err != nil {
		scopedLog.WarnContext(ctx, "Unable to list Gateways", logfields.Error, err)
		return nil
	}

	var gateways []*gatewayv1.Gateway
	for _, gw := range gwList.Items {
		for _, l := range gw.Spec.Listeners {
			if l.TLS == nil {
				continue
			}

			for _, cert := range l.TLS.CertificateRefs {
				if !helpers.IsSecret(cert) {
					continue
				}
				ns := helpers.NamespaceDerefOr(cert.Namespace, gw.GetNamespace())
				if string(cert.Name) == obj.GetName() && ns == obj.GetNamespace() {
					gateways = append(gateways, &gw)
				}
			}
		}
	}
	return gateways
}

func getGatewaysForNamespace(ctx context.Context, c client.Client, ns client.Object, logger *slog.Logger) []types.NamespacedName {
	scopedLog := logger.With(
		logfields.K8sNamespace, ns.GetName(),
	)

	gwList := &gatewayv1.GatewayList{}
	if err := c.List(ctx, gwList); err != nil {
		scopedLog.WarnContext(ctx, "Unable to list Gateways", logfields.Error, err)
		return nil
	}

	var gateways []types.NamespacedName
	for _, gw := range gwList.Items {
		for _, l := range gw.Spec.Listeners {
			if l.AllowedRoutes == nil || l.AllowedRoutes.Namespaces == nil {
				continue
			}

			switch *l.AllowedRoutes.Namespaces.From {
			case gatewayv1.NamespacesFromAll:
				gateways = append(gateways, client.ObjectKey{
					Namespace: gw.GetNamespace(),
					Name:      gw.GetName(),
				})
			case gatewayv1.NamespacesFromSame:
				if ns.GetName() == gw.GetNamespace() {
					gateways = append(gateways, client.ObjectKey{
						Namespace: gw.GetNamespace(),
						Name:      gw.GetName(),
					})
				}
			case gatewayv1.NamespacesFromSelector:
				nsList := &corev1.NamespaceList{}
				err := c.List(ctx, nsList, client.MatchingLabels(l.AllowedRoutes.Namespaces.Selector.MatchLabels))
				if err != nil {
					scopedLog.WarnContext(ctx, "Unable to list Namespaces", logfields.Error, err)
					return nil
				}
				for _, item := range nsList.Items {
					if item.GetName() == ns.GetName() {
						gateways = append(gateways, client.ObjectKey{
							Namespace: gw.GetNamespace(),
							Name:      gw.GetName(),
						})
					}
				}
			}
		}
	}
	return gateways
}

// onlyStatusChanged returns true if and only if there is status change for underlying objects.
// Supported objects are GatewayClass, Gateway, HTTPRoute and GRPCRoute
func onlyStatusChanged() predicate.Predicate {
	option := cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")
	return predicate.Funcs{
		UpdateFunc: func(e event.UpdateEvent) bool {
			switch e.ObjectOld.(type) {
			case *gatewayv1.GatewayClass:
				o, _ := e.ObjectOld.(*gatewayv1.GatewayClass)
				n, ok := e.ObjectNew.(*gatewayv1.GatewayClass)
				if !ok {
					return false
				}
				return !cmp.Equal(o.Status, n.Status, option)
			case *gatewayv1.Gateway:
				o, _ := e.ObjectOld.(*gatewayv1.Gateway)
				n, ok := e.ObjectNew.(*gatewayv1.Gateway)
				if !ok {
					return false
				}
				return !cmp.Equal(o.Status, n.Status, option)
			case *gatewayv1.HTTPRoute:
				o, _ := e.ObjectOld.(*gatewayv1.HTTPRoute)
				n, ok := e.ObjectNew.(*gatewayv1.HTTPRoute)
				if !ok {
					return false
				}
				return !cmp.Equal(o.Status, n.Status, option)
			case *gatewayv1alpha2.TLSRoute:
				o, _ := e.ObjectOld.(*gatewayv1alpha2.TLSRoute)
				n, ok := e.ObjectNew.(*gatewayv1alpha2.TLSRoute)
				if !ok {
					return false
				}
				return !cmp.Equal(o.Status, n.Status, option)
			case *gatewayv1.GRPCRoute:
				o, _ := e.ObjectOld.(*gatewayv1.GRPCRoute)
				n, ok := e.ObjectNew.(*gatewayv1.GRPCRoute)
				if !ok {
					return false
				}
				return !cmp.Equal(o.Status, n.Status, option)
			default:
				return false
			}
		},
	}
}
