// Copyright Istio Authors
//
// 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 agentgateway

import (
	"bytes"
	"fmt"

	"github.com/agentgateway/agentgateway/api"
	"go.uber.org/atomic"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	istio "istio.io/api/networking/v1alpha3"
	"istio.io/istio/pilot/pkg/config/kube/gatewaycommon"
	"istio.io/istio/pilot/pkg/util/protoconv"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/kube/krt"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/ptr"
	"istio.io/istio/pkg/revisions"
	"istio.io/istio/pkg/slices"
)

// Used by agentgateway controller
// TLSInfo contains the TLS certificate and key for a gateway listener.
type TLSInfo struct {
	Cert   []byte
	CaCert []byte
	Key    []byte `json:"-"`
}

// Used by agentgateway controller
type GatewayListener struct {
	Name string
	// The Gateway this listener is bound to
	ParentGateway types.NamespacedName
	// The actual real parent (could be a ListenerSet)
	ParentObject AgwParentKey
	ParentInfo   AgwParentInfo
	TLSInfo      *TLSInfo
	Valid        bool
}

func (g GatewayListener) ResourceName() string {
	return g.Name
}

func (g GatewayListener) Equals(other GatewayListener) bool {
	if (g.TLSInfo != nil) != (other.TLSInfo != nil) {
		return false
	}
	if g.TLSInfo != nil {
		if !bytes.Equal(g.TLSInfo.Cert, other.TLSInfo.Cert) ||
			!bytes.Equal(g.TLSInfo.Key, other.TLSInfo.Key) ||
			!bytes.Equal(g.TLSInfo.CaCert, other.TLSInfo.CaCert) {
			return false
		}
	}
	return g.Valid == other.Valid &&
		g.Name == other.Name &&
		g.ParentGateway == other.ParentGateway &&
		g.ParentObject == other.ParentObject &&
		g.ParentInfo.Equals(other.ParentInfo)
}

func (g AgwParentInfo) Equals(other AgwParentInfo) bool {
	return g.ParentGateway == other.ParentGateway &&
		g.InternalName == other.InternalName &&
		g.OriginalHostname == other.OriginalHostname &&
		g.SectionName == other.SectionName &&
		g.Port == other.Port &&
		g.Protocol == other.Protocol &&
		g.TLSPassthrough == other.TLSPassthrough &&
		slices.EqualFunc(g.AllowedKinds, other.AllowedKinds, func(a, b gatewayv1.RouteGroupKind) bool {
			return a.Kind == b.Kind && ptr.Equal(a.Group, b.Group)
		}) &&
		slices.Equal(g.Hostnames, other.Hostnames)
}

// Borrowed from kgateway
type ListenerSet struct {
	Name string `json:"name"`
	// +krtEqualsTodo include parent gateway identity in equality check
	Parent types.NamespacedName `json:"parent"`
	// +krtEqualsTodo ensure parent metadata differences trigger equality
	ParentInfo    AgwParentInfo        `json:"parentInfo"`
	TLSInfo       *TLSInfo             `json:"tlsInfo"`
	GatewayParent types.NamespacedName `json:"gatewayParent"`
	Valid         bool                 `json:"valid"`
}

func (g ListenerSet) ResourceName() string {
	return g.Name
}

func (g ListenerSet) Equals(other ListenerSet) bool {
	if (g.TLSInfo != nil) != (other.TLSInfo != nil) {
		return false
	}
	if g.TLSInfo != nil {
		if !bytes.Equal(g.TLSInfo.Cert, other.TLSInfo.Cert) ||
			!bytes.Equal(g.TLSInfo.Key, other.TLSInfo.Key) ||
			!bytes.Equal(g.TLSInfo.CaCert, other.TLSInfo.CaCert) {
			return false
		}
	}
	return g.Name == other.Name &&
		g.Parent == other.Parent &&
		g.ParentInfo.Equals(other.ParentInfo) &&
		g.GatewayParent == other.GatewayParent &&
		g.Valid == other.Valid
}

func ListenerSetCollection(
	listenerSets krt.Collection[*gatewayv1.ListenerSet],
	gateways krt.Collection[*gatewayv1.Gateway],
	gatewayConflicts krt.Collection[gatewaycommon.GatewayListenerConflicts],
	gatewayClasses krt.Collection[gatewaycommon.GatewayClass],
	namespaces krt.Collection[*corev1.Namespace],
	grants gatewaycommon.ReferenceGrants,
	configMaps krt.Collection[*corev1.ConfigMap],
	secrets krt.Collection[*corev1.Secret],
	domainSuffix string,
	gatewayContext krt.RecomputeProtected[*atomic.Pointer[gatewaycommon.GatewayContext]],
	tagWatcher krt.RecomputeProtected[revisions.TagWatcher],
	opts krt.OptionsBuilder,
) (
	krt.StatusCollection[*gatewayv1.ListenerSet, gatewayv1.ListenerSetStatus],
	krt.Collection[ListenerSet],
) {
	// Note: tagWatcher.IsMine() is intentionally not filtered at this config-emission layer. Filtering
	// here caused a temporary outage when a Gateway's or ListenerSet's istio.io/rev label was changed:
	// the prior owning control plane immediately dropped the resource and pushed empty xDS config to
	// pods still running on the old revision (see https://github.com/istio/istio/issues/59959). Status
	// writes are filtered to the owning revision via RegisterStatus in pilot/pkg/status/collections.go,
	// and Deployment management is filtered in pilot/pkg/config/kube/gatewaycommon/deploymentcontroller.go.
	statusCol, gw := krt.NewStatusManyCollection(listenerSets,
		func(ctx krt.HandlerContext, obj *gatewayv1.ListenerSet) (*gatewayv1.ListenerSetStatus, []ListenerSet) {
			// We currently depend on service discovery information not known to krt; mark we depend on it.
			context := gatewayContext.Get(ctx).Load()
			if context == nil {
				return nil, nil
			}
			result := []ListenerSet{}
			ls := obj.Spec
			status := obj.Status.DeepCopy()

			p := ls.ParentRef
			if normalizeReference(p.Group, p.Kind, gvk.KubernetesGateway) != gvk.KubernetesGateway {
				// Cannot report status since we don't know if it is for us
				return nil, nil
			}

			pns := ptr.OrDefault(p.Namespace, gatewayv1.Namespace(obj.Namespace))
			parentGwObj := ptr.Flatten(krt.FetchOne(ctx, gateways, krt.FilterKey(string(pns)+"/"+string(p.Name))))
			if parentGwObj == nil {
				// Cannot report status since we don't know if it is for us
				return nil, nil
			}

			class := gatewaycommon.FetchAgentgatewayClass(ctx, gatewayClasses, parentGwObj.Spec.GatewayClassName)
			if class == nil {
				// Cannot report status since we don't know if it is for us
				return nil, nil
			}

			controllerName := class.Controller
			classInfo, f := gatewaycommon.ClassInfos[controllerName]
			if !f {
				// Cannot report status since we don't know if it is for us
				return nil, nil
			}

			if !classInfo.SupportsListenerSet {
				gatewaycommon.ReportUnsupportedListenerSet(class.Name, status, obj)
				return status, nil
			}

			if !gatewaycommon.NamespaceAcceptedByAllowListeners(obj.Namespace, parentGwObj, func(s string) *corev1.Namespace {
				return ptr.Flatten(krt.FetchOne(ctx, namespaces, krt.FilterKey(s)))
			}) {
				gatewaycommon.ReportNotAllowedListenerSet(status, obj)
				return status, nil
			}

			gatewayServices, err := extractGatewayServices(domainSuffix, parentGwObj, classInfo)
			if len(gatewayServices) == 0 && err != nil {
				// Short circuit if it's a hard failure
				gatewaycommon.ReportListenerSetStatus(context, parentGwObj, obj, status, gatewayServices, nil, listenerSetParentErr(err), true)
				return status, nil
			}

			var conflicts map[gatewayv1.SectionName]gatewayv1.ListenerConditionReason
			if gwConflict := krt.FetchOne(ctx, gatewayConflicts, krt.FilterKey(config.NamespacedName(parentGwObj).String())); gwConflict != nil {
				conflicts = gwConflict.ConflictsFor(obj)
			}

			servers := []*istio.Server{}
			validListeners := 0
			standardStatus := slices.Map(status.Listeners, gatewaycommon.ConvertListenerSetStatusToStandardStatus)
			for i, l := range ls.Listeners {
				port, portErr := gatewaycommon.ListenerEntryPortNumber(l)
				l.Port = port
				standardListener := gatewaycommon.ConvertListenerSetToListener(l)

				if reason, ok := conflicts[l.Name]; ok {
					standardStatus = gatewaycommon.ReportListenerConflict(i, standardListener, obj, standardStatus, reason)
					continue
				}

				server, tlsInfo, updatedStatus, programmed := buildListener(ctx, secrets, configMaps, grants, namespaces,
					obj, standardStatus, parentGwObj.Spec, standardListener, i, controllerName, portErr)
				standardStatus = updatedStatus

				if server == nil {
					continue
				}

				if programmed {
					validListeners++
				}
				servers = append(servers, server)

				// TODO(jaellio): Figure out if this logic needs to change to support agentgateway as an E/W gateway
				if controllerName == constants.ManagedGatewayMeshController || controllerName == constants.ManagedGatewayEastWestController {
					// Waypoint doesn't actually convert the routes to VirtualServices
					continue
				}
				name := InternalGatewayName(obj.Namespace, obj.Name, string(l.Name))

				allowed, _ := gatewaycommon.GenerateSupportedKinds(standardListener)
				pri := AgwParentInfo{
					ParentGateway:    config.NamespacedName(parentGwObj),
					InternalName:     obj.Namespace + "/" + name,
					AllowedKinds:     allowed,
					Hostnames:        server.Hosts,
					OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
					SectionName:      l.Name,
					Port:             l.Port,
					Protocol:         l.Protocol,
					TLSPassthrough:   l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == gatewayv1.TLSModePassthrough,
				}

				res := ListenerSet{
					Name:          name,
					Valid:         programmed,
					TLSInfo:       tlsInfo,
					Parent:        config.NamespacedName(obj),
					GatewayParent: config.NamespacedName(parentGwObj),
					ParentInfo:    pri,
				}
				result = append(result, res)
			}

			status.Listeners = slices.Map(standardStatus, gatewaycommon.ConvertStandardStatusToListenerSetStatus)
			gatewaycommon.ReportListenerSetStatus(context, parentGwObj, obj, status, gatewayServices, servers, listenerSetParentErr(err), validListeners > 0)
			return status, result
		}, opts.WithName("ListenerSets")...)

	return statusCol, gw
}

func listenerSetParentErr(err *Condition) *gatewaycommon.ListenerStatusConfigError {
	if err == nil || err.error == nil {
		return nil
	}
	return &gatewaycommon.ListenerStatusConfigError{Reason: err.error.Reason, Message: err.error.Message}
}

// GatewayCollection builds the collection of Gateways. This is responsible for translating from the Kubernetes Gateway API
// types to the internal representation used by the agent gateway, as well as computing the status for the Gateways. The
// listeners are processed in a separate collection (ListenerSetCollection) since they have their own status that needs to
// be computed separately.
func GatewayCollection(
	gateways krt.Collection[*gatewayv1.Gateway],
	listenerSets krt.Collection[ListenerSet],
	gatewayConflicts krt.Collection[gatewaycommon.GatewayListenerConflicts],
	gatewayClasses krt.Collection[gatewaycommon.GatewayClass],
	namespaces krt.Collection[*corev1.Namespace],
	grants gatewaycommon.ReferenceGrants,
	configMaps krt.Collection[*corev1.ConfigMap],
	secrets krt.Collection[*corev1.Secret],
	domainSuffix string,
	gatewayContext krt.RecomputeProtected[*atomic.Pointer[gatewaycommon.GatewayContext]],
	tagWatcher krt.RecomputeProtected[revisions.TagWatcher],
	opts krt.OptionsBuilder,
) (
	krt.StatusCollection[*gatewayv1.Gateway, gatewayv1.GatewayStatus],
	krt.Collection[*GatewayListener],
) {
	listenerIndex := krt.NewIndex(listenerSets, "gatewayParent", func(o ListenerSet) []types.NamespacedName {
		return []types.NamespacedName{o.GatewayParent}
	})
	// Note: tagWatcher.IsMine() is intentionally not filtered at this config-emission layer. See the
	// comment in ListenerSetCollection above for the rationale.
	gwstatus, gw := krt.NewStatusManyCollection(gateways, func(ctx krt.HandlerContext, obj *gatewayv1.Gateway) (*gatewayv1.GatewayStatus, []*GatewayListener) {
		context := gatewayContext.Get(ctx).Load()
		if context == nil {
			return nil, nil
		}
		result := []*GatewayListener{}
		kgw := obj.Spec
		status := obj.Status.DeepCopy()

		class := gatewaycommon.FetchAgentgatewayClass(ctx, gatewayClasses, kgw.GatewayClassName)
		if class == nil {
			return nil, nil
		}

		controllerName := class.Controller
		classInfo, f := gatewaycommon.ClassInfos[controllerName]
		if !f {
			return nil, nil
		}
		if classInfo.DisableRouteGeneration {
			// TODO(jaellio): Handle reporting on unmanaged agentgateways
			// We found it, but don't want to handle this class
			return status, nil
		}
		servers := []*istio.Server{}

		log.Debugf("translating Gateway gw_name: %s, resource_version: %s", obj.GetName(), obj.GetResourceVersion())

		// Extract the addresses. A gateway will bind to a specific Service
		gatewayServices, err := extractGatewayServices(domainSuffix, obj, classInfo)
		if len(gatewayServices) == 0 && err != nil {
			// Short circuit if its a hard failure
			log.Errorf("failed to translate gwv1", "name", obj.GetName(), "namespace", obj.GetNamespace(), "err", err.message)
			reportGatewayStatus(context, obj, status, classInfo, gatewayServices, servers, 0, err.error)
			return status, nil
		}
		var gatewayErr *ConfigError
		if err != nil {
			gatewayErr = err.error
		}

		var gwListenerConflicts map[gatewayv1.SectionName]gatewayv1.ListenerConditionReason
		if gwConflict := krt.FetchOne(ctx, gatewayConflicts, krt.FilterKey(config.NamespacedName(obj).String())); gwConflict != nil {
			gwListenerConflicts = gwConflict.ConflictsForGateway(obj)
		}

		for i, l := range kgw.Listeners {
			if reason, ok := gwListenerConflicts[l.Name]; ok {
				status.Listeners = gatewaycommon.ReportListenerConflict(i, l, obj, status.Listeners, reason)
				continue
			}

			// Attached Routes count starts at 0 and gets updated later in the status syncer
			// when the real count is available after route processing
			server, tlsInfo, updatedStatus, programmed := buildListener(
				ctx, secrets, configMaps, grants, namespaces, obj, status.Listeners, kgw, l, i, controllerName, nil)
			status.Listeners = updatedStatus

			if server == nil {
				continue
			}

			servers = append(servers, server)

			// Generate supported kinds for the listener
			allowed, _ := gatewaycommon.GenerateSupportedKinds(l)

			name := InternalGatewayName(obj.Namespace, obj.Name, string(l.Name))
			pri := AgwParentInfo{
				ParentGateway:          config.NamespacedName(obj),
				ParentGatewayClassName: string(obj.Spec.GatewayClassName),
				InternalName:           InternalGatewayName(obj.Namespace, name, ""),
				AllowedKinds:           allowed,
				Hostnames:              server.Hosts,
				OriginalHostname:       string(ptr.OrEmpty(l.Hostname)),
				SectionName:            l.Name,
				Port:                   l.Port,
				Protocol:               l.Protocol,
				TLSPassthrough:         l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == gatewayv1.TLSModePassthrough,
			}

			res := &GatewayListener{
				Name:          name,
				Valid:         programmed,
				TLSInfo:       tlsInfo,
				ParentGateway: config.NamespacedName(obj),
				ParentObject: AgwParentKey{
					Kind:      gvk.KubernetesGateway.Kubernetes(),
					Name:      obj.Name,
					Namespace: obj.Namespace,
				},
				ParentInfo: pri,
			}
			result = append(result, res)
		}
		listenersFromSets := listenerIndex.Fetch(ctx, config.NamespacedName(obj))
		// When reporting gateway status, we need to count the actual ListenerSets attached to the gateway
		// and not the listeners
		listenerSets := make(map[types.NamespacedName]bool)
		for _, ls := range listenersFromSets {
			if !ls.Valid {
				continue
			}
			listenerSets[ls.Parent] = true
			result = append(result, &GatewayListener{
				Name:          ls.Name,
				ParentGateway: config.NamespacedName(obj),
				ParentObject: AgwParentKey{
					Kind:      gvk.ListenerSet.Kubernetes(),
					Name:      ls.Parent.Name,
					Namespace: ls.Parent.Namespace,
				},
				TLSInfo:    ls.TLSInfo,
				ParentInfo: ls.ParentInfo,
				Valid:      ls.Valid,
			})
		}

		reportGatewayStatus(context, obj, status, classInfo, gatewayServices, servers, len(listenerSets), gatewayErr)
		return status, result
	}, opts.WithName("KubernetesGateway")...)

	return gwstatus, gw
}

// InternalGatewayName returns the name of the internal Gateway corresponding to the
// specified gwv1-api gwv1 and listener. If the listener is not specified, returns internal name without listener.
// Format: gwNs/gwName.listener
func InternalGatewayName(gwNamespace, gwName, lName string) string {
	if lName == "" {
		return fmt.Sprintf("%s/%s", gwNamespace, gwName)
	}
	return fmt.Sprintf("%s/%s.%s", gwNamespace, gwName, lName)
}

// TODO(jaellio): Move these definitions to route collection (?)
// RouteParents holds information about things routes can reference as parents.
type RouteParents struct {
	gatewayIndex krt.Index[AgwParentKey, *GatewayListener]
}

func (p RouteParents) fetch(ctx krt.HandlerContext, pk AgwParentKey) []*AgwParentInfo {
	return slices.Map(p.gatewayIndex.Fetch(ctx, pk), func(gw *GatewayListener) *AgwParentInfo {
		return &gw.ParentInfo
	})
}

func BuildRouteParents(
	gateways krt.Collection[*GatewayListener],
) RouteParents {
	idx := krt.NewIndex(gateways, "parent", func(o *GatewayListener) []AgwParentKey {
		return []AgwParentKey{o.ParentObject}
	})
	return RouteParents{
		gatewayIndex: idx,
	}
}

// AgwRoute is a wrapper type that contains the route on the gateway, as well as the status for the route.
// A Route is a special resource that represents a route for an HTTP listener, which has different semantics
// than a TCP route and needs to be tracked separately.
type AgwRoute struct {
	*api.Route
}

func (g AgwRoute) ResourceName() string {
	return g.Key
}

func (g AgwRoute) Equals(other AgwRoute) bool {
	return protoconv.Equals(g, other)
}

// AgwTCPRoute is a wrapper type that contains the tcp route on the gateway, as well as the status for the tcp route.
// A TCPRoute is a special resource that represents a route for a TCP listener, which has different semantics than an
// HTTP route and needs to be tracked separately.
type AgwTCPRoute struct {
	*api.TCPRoute
}

func (g AgwTCPRoute) ResourceName() string {
	return g.Key
}

func (g AgwTCPRoute) Equals(other AgwTCPRoute) bool {
	return protoconv.Equals(g, other)
}

// AgwBind is a wrapper type that contains the bind on the gateway, as well as the status for the bind.
// A Bind is a special resource that represents the binding of a route to a listener. It is used to track
// the attached routes for a listener, which is information that is not available until route processing.
type AgwBind struct {
	*api.Bind
}

func (g AgwBind) ResourceName() string {
	return g.Key
}

func (g AgwBind) Equals(other AgwBind) bool {
	return protoconv.Equals(g, other)
}

// AgwListener is a wrapper type that contains the listener on the gateway, as well as the status for the listener.
type AgwListener struct {
	*api.Listener
}

func (g AgwListener) ResourceName() string {
	return g.Key
}

func (g AgwListener) Equals(other AgwListener) bool {
	return protoconv.Equals(g, other)
}
