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

// nolint: gocritic
package ambient

import (
	"fmt"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	"istio.io/api/label"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/serviceregistry/kube"
	"istio.io/istio/pkg/cluster"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/kube/krt"
	"istio.io/istio/pkg/kube/multicluster"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/network"
	"istio.io/istio/pkg/ptr"
)

type NetworkGateway struct {
	model.NetworkGateway
	Source types.NamespacedName
}

func (n NetworkGateway) ResourceName() string {
	return n.Source.String() + "/" + n.Addr
}

type NetworkCollections struct {
	LocalSystemNamespace krt.Singleton[ClusterNetwork]
	NetworkGateways      krt.Collection[NetworkGateway]
	GatewaysByNetwork    krt.Index[network.ID, NetworkGateway]
	// systemNamespace is the namespace whose topology label names the network a cluster belongs to.
	systemNamespace string
}

type ClusterNetwork struct {
	ClusterID cluster.ID
	Network   network.ID
}

func (c ClusterNetwork) ResourceName() string {
	return fmt.Sprintf("%s/%s", c.ClusterID, c.Network)
}

// FetchLocalNetworkID fetches the local network ID from a ClusterNetwork singleton within a KRT handler context.
func (c NetworkCollections) FetchLocalNetworkID(ctx krt.HandlerContext) network.ID {
	return ptr.OrEmpty(krt.FetchOne(ctx, c.LocalSystemNamespace.AsCollection())).Network
}

// FetchRemoteSystemNamespaceNetwork returns the network a remote cluster belongs to, read from the topology
// label on its system namespace. A cluster whose system namespace is absent, or carries no label, is
// part of the default (empty) network.
func (c NetworkCollections) FetchRemoteSystemNamespaceNetwork(ctx krt.HandlerContext, namespaces krt.Collection[*v1.Namespace]) network.ID {
	nw := ptr.Flatten(krt.FetchOne(ctx, namespaces, krt.FilterKey(c.systemNamespace)))
	if nw == nil {
		return ""
	}
	return network.ID(nw.Labels[label.TopologyNetwork.Name])
}

func (c NetworkCollections) HasSynced() bool {
	return c.LocalSystemNamespace.AsCollection().HasSynced() &&
		c.NetworkGateways.HasSynced()
}

func buildGlobalNetworkCollections(
	ctrl *multicluster.Controller,
	localNamespaces krt.Collection[*v1.Namespace],
	localGateways krt.Collection[*gatewayv1.Gateway],
	gateways krt.Collection[krt.Collection[krt.ObjectWithCluster[*gatewayv1.Gateway]]],
	gatewaysByCluster krt.Index[cluster.ID, krt.Collection[krt.ObjectWithCluster[*gatewayv1.Gateway]]],
	options Options,
	opts krt.OptionsBuilder,
) NetworkCollections {
	LocalSystemNamespaceNetwork := krt.NewSingleton(func(ctx krt.HandlerContext) *ClusterNetwork {
		ns := ptr.Flatten(krt.FetchOne(ctx, localNamespaces, krt.FilterKey(options.SystemNamespace)))
		if ns == nil {
			return nil
		}
		nw, f := ns.Labels[label.TopologyNetwork.Name]
		if !f {
			return nil
		}
		return &ClusterNetwork{
			ClusterID: options.ClusterID,
			Network:   network.ID(nw),
		}
	}, opts.WithName("LocalSystemNamespaceNetwork")...)
	// N.B there is deliberately no aggregated collection of remote clusters' networks. A cluster's
	// network is read straight from its own namespaces collection, by the per-cluster collections that
	// need it, via FetchRemoteSystemNamespaceNetwork.
	localNetworkGateways := krt.NewManyCollection(localGateways, func(ctx krt.HandlerContext, gw *gatewayv1.Gateway) []krt.ObjectWithCluster[NetworkGateway] {
		return k8sGatewayToNetworkGatewaysWithCluster(options.ClusterID, gw, options.ClusterID)
	}, opts.WithName("LocalNetworkGateways")...)

	GlobalNetworkGateways := multicluster.NestedCollectionFromLocalAndRemote(
		ctrl,
		localNetworkGateways,
		func(ctx krt.HandlerContext, c *multicluster.Cluster) *krt.Collection[krt.ObjectWithCluster[NetworkGateway]] {
			opts := []krt.CollectionOption{
				krt.WithName(fmt.Sprintf("NetworkGateways[%s]", c.ID)),
				krt.WithDebugging(opts.Debugger()),
				krt.WithStop(c.GetStop()),
				krt.WithMetadata(krt.Metadata{
					multicluster.ClusterKRTMetadataKey: c.ID,
				}),
			}
			gatewaysPtr := krt.FetchOne(ctx, gateways, krt.FilterIndex(gatewaysByCluster, c.ID))
			if gatewaysPtr == nil {
				log.Warnf("No gateways found for cluster %s", c.ID)
				return nil
			}
			gateways := *gatewaysPtr
			// We can't have duplicate collections (otherwise FetchOne will panic) so use
			// sync.Once to ensure we only create the collection once and return that same value
			nwGateways := krt.NewManyCollection(
				gateways,
				func(ctx krt.HandlerContext, gw krt.ObjectWithCluster[*gatewayv1.Gateway]) []krt.ObjectWithCluster[NetworkGateway] {
					innerGw := ptr.Flatten(gw.Object)
					if innerGw == nil {
						return nil
					}
					return k8sGatewayToNetworkGatewaysWithCluster(c.ID, innerGw, options.ClusterID)
				},
				opts...,
			)

			return ptr.Of(nwGateways)
		},
		"NetworkGateways",
		opts,
	)
	MergedNetworkGatewaysWithCluster := krt.NestedJoinWithMergeCollection(
		GlobalNetworkGateways,
		func(gateways []krt.ObjectWithCluster[NetworkGateway]) *krt.ObjectWithCluster[NetworkGateway] {
			if len(gateways) == 0 {
				return nil
			}
			for _, gw := range gateways {
				if gw.Object == nil {
					continue
				}
				if gw.ClusterID == options.ClusterID {
					// If we have a duplicate resource name and address (which forms the key),
					// we want to use the one from the local cluster.
					return &gw
				}
			}

			// No local cluster found, return the first one
			return &gateways[0]
		},
		opts.WithName("MergedGlobalNetworkGatewaysWithCluster")...,
	)

	MergedNetworkGateways := krt.MapCollection(MergedNetworkGatewaysWithCluster, unwrapObjectWithCluster, opts.WithName("MergedGlobalNetworkGateways")...)

	GatewaysByNetwork := krt.NewIndex(MergedNetworkGateways, "network", func(o NetworkGateway) []network.ID {
		return []network.ID{o.Network}
	})

	return NetworkCollections{
		NetworkGateways:      MergedNetworkGateways,
		GatewaysByNetwork:    GatewaysByNetwork,
		LocalSystemNamespace: LocalSystemNamespaceNetwork,
		systemNamespace:      options.SystemNamespace,
	}
}

func BuildNetworkCollections(
	namespaces krt.Collection[*v1.Namespace],
	gateways krt.Collection[*gatewayv1.Gateway],
	options Options,
	opts krt.OptionsBuilder,
) NetworkCollections {
	SystemNamespaceNetwork := krt.NewSingleton(func(ctx krt.HandlerContext) *ClusterNetwork {
		ns := ptr.Flatten(krt.FetchOne(ctx, namespaces, krt.FilterKey(options.SystemNamespace)))
		if ns == nil {
			return nil
		}
		nw, f := ns.Labels[label.TopologyNetwork.Name]
		if !f {
			return nil
		}
		return &ClusterNetwork{
			ClusterID: options.ClusterID,
			Network:   network.ID(nw),
		}
	}, opts.WithName("SystemNamespaceNetwork")...)
	NetworkGateways := krt.NewManyCollection(
		gateways,
		fromGatewayBuilder(options.ClusterID, options.ClusterID),
		opts.WithName("NetworkGateways")...,
	)
	GatewaysByNetwork := krt.NewIndex(NetworkGateways, "network", func(o NetworkGateway) []network.ID {
		return []network.ID{o.Network}
	})

	return NetworkCollections{
		LocalSystemNamespace: SystemNamespaceNetwork,
		NetworkGateways:      NetworkGateways,
		GatewaysByNetwork:    GatewaysByNetwork,
		systemNamespace:      options.SystemNamespace,
	}
}

func k8sGatewayToNetworkGatewaysWithCluster(
	clusterID cluster.ID,
	gw *gatewayv1.Gateway,
	localClusterID cluster.ID,
) []krt.ObjectWithCluster[NetworkGateway] {
	gateways := k8sGatewayToNetworkGateways(clusterID, gw, localClusterID)
	var networkGateways []krt.ObjectWithCluster[NetworkGateway]
	for _, gateway := range gateways {
		networkGateways = append(networkGateways, krt.ObjectWithCluster[NetworkGateway]{
			ClusterID: clusterID,
			Object:    &gateway,
		})
	}

	return networkGateways
}

func remoteK8sGatewayToNetworkGateways(clusterID cluster.ID, gw *gatewayv1.Gateway) []NetworkGateway {
	netLabel := gw.GetLabels()[label.TopologyNetwork.Name]
	if netLabel == "" {
		return nil
	}

	// This is a remote east/west gateway, so check that the gateway class is correct
	if gw.Spec.GatewayClassName != constants.EastWestGatewayClassName {
		return nil
	}

	base := model.NetworkGateway{
		Network: network.ID(netLabel),
		Cluster: clusterID,
		ServiceAccount: types.NamespacedName{
			Namespace: gw.Namespace,
			Name:      kube.GatewaySA(gw),
		},
	}
	gateways := []NetworkGateway{}
	source := config.NamespacedName(gw)
	for _, addr := range gw.Status.Addresses {
		if addr.Type == nil {
			continue
		}
		if addrType := *addr.Type; addrType != gatewayv1.IPAddressType && addrType != gatewayv1.HostnameAddressType {
			continue
		}
		for _, l := range gw.Spec.Listeners {
			tlsModeTerminate := l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == gatewayv1.TLSModeTerminate
			if l.Protocol == "HBONE" && tlsModeTerminate {
				networkGateway := base
				networkGateway.Addr = addr.Value
				gateways = append(gateways, NetworkGateway{
					NetworkGateway: networkGateway,
					Source:         source,
				})
				break
			}
		}
	}
	return gateways
}

func localK8sGatewayToNetworkGateways(clusterID cluster.ID, gw *gatewayv1.Gateway) []NetworkGateway {
	netLabel := gw.GetLabels()[label.TopologyNetwork.Name]
	if netLabel == "" {
		return nil
	}

	if gw.Spec.GatewayClassName != constants.RemoteGatewayClassName {
		return nil
	}

	base := model.NetworkGateway{
		Network: network.ID(netLabel),
		Cluster: clusterID,
		ServiceAccount: types.NamespacedName{
			Namespace: gw.Namespace,
			Name:      kube.GatewaySA(gw),
		},
	}
	gateways := []NetworkGateway{}
	source := config.NamespacedName(gw)
	for _, addr := range gw.Status.Addresses {
		if addr.Type == nil {
			continue
		}
		if addrType := *addr.Type; addrType != gatewayv1.IPAddressType && addrType != gatewayv1.HostnameAddressType {
			continue
		}
		for _, l := range gw.Spec.Listeners {
			if l.Protocol == "HBONE" {
				networkGateway := base
				networkGateway.Addr = addr.Value
				networkGateway.Port = uint32(l.Port)
				networkGateway.HBONEPort = uint32(l.Port)
				gateways = append(gateways, NetworkGateway{
					NetworkGateway: networkGateway,
					Source:         source,
				})
				break
			}
		}
	}
	return gateways
}

func k8sGatewayToNetworkGateways(clusterID cluster.ID, gw *gatewayv1.Gateway, localClusterID cluster.ID) []NetworkGateway {
	if clusterID != localClusterID {
		// This is a gateway in a remote cluster, use different logic
		return remoteK8sGatewayToNetworkGateways(clusterID, gw)
	}

	return localK8sGatewayToNetworkGateways(clusterID, gw)
}

func fromGatewayBuilder(clusterID, localClusterID cluster.ID) krt.TransformationMulti[*gatewayv1.Gateway, NetworkGateway] {
	return func(ctx krt.HandlerContext, gw *gatewayv1.Gateway) []NetworkGateway {
		return k8sGatewayToNetworkGateways(clusterID, gw, localClusterID)
	}
}
