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

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"

	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	xdsfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3"
	cors "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
	extproc "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
	xdshttpfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
	statefulsession "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/stateful_session/v3"
	matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
	xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/wrapperspb"
	"k8s.io/apimachinery/pkg/types"

	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	istionetworking "istio.io/istio/pilot/pkg/networking"
	"istio.io/istio/pilot/pkg/networking/core/route/retry"
	"istio.io/istio/pilot/pkg/networking/telemetry"
	"istio.io/istio/pilot/pkg/networking/util"
	authz "istio.io/istio/pilot/pkg/security/authz/model"
	"istio.io/istio/pilot/pkg/util/protoconv"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/gateway/kube"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/config/protocol"
	"istio.io/istio/pkg/jwt"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/util/grpc"
	"istio.io/istio/pkg/util/sets"
	"istio.io/istio/pkg/wellknown"
)

// Headers with special meaning in Envoy
const (
	HeaderMethod    = ":method"
	HeaderAuthority = ":authority"
	HeaderScheme    = ":scheme"
)

// DefaultRouteName is the name assigned to a route generated by default in absence of a virtual service.
const DefaultRouteName = "default"

var Notimeout = durationpb.New(0)

// DefaultMaxDirectResponseBodySizeBytes is 1mb, the same limit the control plane validates via webhook. Set this to increase from envoy default of 4k
var DefaultMaxDirectResponseBodySizeBytes = wrapperspb.UInt32(1024 * 1024)

type DestinationHashMap map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB

// VirtualHostWrapper is a context-dependent virtual host entry with guarded routes.
// Note: Currently we are not fully utilizing this structure. We could invoke this logic
// once for all sidecars in the cluster to compute all RDS for inside the mesh and arrange
// it by listener port. However to properly use such an optimization, we need to have an
// eventing subsystem to invalidate the computed routes if any service changes/virtual Services change.
type VirtualHostWrapper struct {
	// Port is the listener port for outbound sidecar (e.g. service port)
	Port int

	// Services are the Services from the registry. Each service
	// in this list should have a virtual host entry
	Services []*model.Service

	// VirtualServiceHosts is a list of hosts defined in the virtual service
	// if virtual service hostname is same as a the service registry host, then
	// the host would appear in Services as we need to generate all variants of the
	// service's hostname within a platform (e.g., foo, foo.default, foo.default.svc, etc.)
	VirtualServiceHosts []string

	// Routes in the virtual host
	Routes []*route.Route
}

// BuildSidecarVirtualHostWrapper creates virtual hosts from the given set of virtual Services
// and a list of Services from the service registry. Services are indexed by FQDN hostnames.
// The list of Services is also passed to allow maintaining consistent ordering.
func BuildSidecarVirtualHostWrapper(routeCache *Cache, node *model.Proxy, push *model.PushContext, serviceRegistry map[host.Name]*model.Service,
	virtualServices []*config.Config, listenPort int, mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName,
) []VirtualHostWrapper {
	out := make([]VirtualHostWrapper, 0)

	// dependentDestinationRules includes all the destinationrules referenced by
	// the virtualservices, which have consistent hash policy.
	dependentDestinationRules := []*model.ConsolidatedDestRule{}

	// First build virtual host wrappers for services that have virtual services.
	for _, virtualService := range virtualServices {
		hashByDestination, destinationRules := hashForVirtualService(push, node, *virtualService)
		dependentDestinationRules = append(dependentDestinationRules, destinationRules...)
		wrappers := buildSidecarVirtualHostsForVirtualService(
			node, virtualService, serviceRegistry, hashByDestination, listenPort, push, mostSpecificWildcardVsIndex,
		)
		out = append(out, wrappers...)
	}

	// Now exclude the services that have virtual services.
	for _, wrapper := range out {
		for _, service := range wrapper.Services {
			delete(serviceRegistry, service.Hostname)
		}
	}

	for _, svc := range serviceRegistry {
		// Filter any aliases out. While we want to be able to use them as the backend to a route, we don't want
		// to have them build standalone route matches; this is already handled.
		// Each alias will get a mapping of 'Alias -> Concrete' service when the concrete service is built
		// if we let this through, we would get 'Alias -> Alias'.
		if svc.Resolution == model.Alias {
			continue
		}
		for _, port := range svc.Ports {
			if port.Protocol.IsHTTPOrSniffed() {
				hash, destinationRule := hashForService(push, node, svc, port)
				if hash != nil {
					dependentDestinationRules = append(dependentDestinationRules, destinationRule)
				}
				// append default hosts for the service missing virtual Services.
				out = append(out, buildSidecarVirtualHostForService(svc, port, hash, push))
			}
		}
	}

	if routeCache != nil {
		routeCache.DestinationRules = dependentDestinationRules
	}

	return out
}

// separateVSHostsAndServices splits the virtual service hosts into Services (if they are found in the registry) and
// plain non-registry hostnames
func separateVSHostsAndServices(virtualService config.Config,
	serviceRegistry map[host.Name]*model.Service,
	mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName,
) ([]string, []*model.Service) {
	// TODO: A further optimization would be to completely rely on the index and not do the loop below
	// However, that requires assuming that serviceRegistry never got filtered after the
	// egressListener was created.
	rule := virtualService.Spec.(*networking.VirtualService)
	// Stores VS hosts that don't correspond to services in the registry
	// Currently, the only use for this list is to enable VirtualService configuration to affect
	// traffic to hosts outside of the service registry (e.g. google.com) on port 80
	nonServiceRegistryHosts := make([]string, 0)
	// Stores services for this VirtualService that are in the registry (based on hostname)
	matchingRegistryServices := make([]*model.Service, 0)
	wchosts := make([]host.Name, 0)

	// As a performance optimization, process non wildcard hosts first, so that they can be
	// looked up directly in the service registry map.
	for _, hostname := range rule.Hosts {
		vshost := host.Name(strings.ToLower(hostname))
		if vshost.IsWildCarded() {
			// We'll process wild card hosts later
			wchosts = append(wchosts, vshost)
			continue
		}
		if svc, exists := serviceRegistry[vshost]; exists {
			matchingRegistryServices = append(matchingRegistryServices, svc)
		} else {
			nonServiceRegistryHosts = append(nonServiceRegistryHosts, hostname)
		}
	}

	// Now process wild card hosts as they need to follow the slow path of looping through all Services in the registry.
	for _, hostname := range wchosts {
		if model.UseGatewaySemantics(virtualService) {
			nonServiceRegistryHosts = append(nonServiceRegistryHosts, string(hostname))
			continue
		}
		// foundSvcMatch's only purpose is to make sure we don't add hosts that correspond to services
		// to the list of non-serviceregistry hosts
		foundSvcMatch := false
		for svcHost, svc := range serviceRegistry {
			// First, check if this service matches the VS host.
			// If it does, then we never want to add it to the nonServiceRegistryHosts list.
			// The result is OR'd so we don't overwrite a previously true value
			// localMatch is tracking the match found within this iteration of the loop
			localMatch := svcHost.Matches(hostname)
			// foundSvcMatch is tracking in the wider context whether or not ANY match was found during an iteration
			foundSvcMatch = foundSvcMatch || localMatch
			if !localMatch {
				// If the wildcard doesn't even match this service, it won't be in the index
				continue
			}
			// The mostSpecificWildcardVsIndex ensures that each VirtualService host is only associated with
			// a single service in the registry. This is generally results in the most specific wildcard match for
			// a given wildcard host.
			vs, ok := mostSpecificWildcardVsIndex[svcHost]
			if !ok {
				// This service doesn't have a virtualService that matches it.
				continue
			}
			if vs != virtualService.NamespacedName() {
				// This virtual service is not the most specific wildcard match for this service.
				// So we don't add it to the list of services in this virtual service so as
				// to avoid duplicates
				continue
			}
			matchingRegistryServices = append(matchingRegistryServices, svc)
		}

		// If we never found a match for this hostname in the service registry, add it to the list of non-service hosts
		if !foundSvcMatch {
			nonServiceRegistryHosts = append(nonServiceRegistryHosts, strings.ToLower(string(hostname)))
		}
	}

	return nonServiceRegistryHosts, matchingRegistryServices
}

// buildSidecarVirtualHostsForVirtualService creates virtual hosts corresponding to a virtual service.
// Called for each port to determine the list of vhosts on the given port.
// It may return an empty list if no VirtualService rule has a matching service.
func buildSidecarVirtualHostsForVirtualService(
	node *model.Proxy,
	virtualService *config.Config,
	serviceRegistry map[host.Name]*model.Service,
	hashByDestination DestinationHashMap,
	listenPort int,
	push *model.PushContext,
	mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName,
) []VirtualHostWrapper {
	meshGateway := sets.New(constants.IstioMeshGateway)

	infPoolConfigs := CheckAndGetInferencePoolConfigs(*virtualService)

	opts := RouteOptions{
		// Sidecar is never terminating TLS
		IsTLS: false,
		// Sidecar is never doing H3 (yet)
		IsHTTP3AltSvcHeaderNeeded: false,
		Mesh:                      push.Mesh,
		Push:                      push,
		LookupService: func(name host.Name) *model.Service {
			return serviceRegistry[name]
		},
		LookupDestinationCluster: GetDestinationCluster,
		LookupHash: func(destination *networking.HTTPRouteDestination) *networking.LoadBalancerSettings_ConsistentHashLB {
			return hashByDestination[destination]
		},
		InferencePoolExtensionRefs: infPoolConfigs,
	}

	routes, err := BuildHTTPRoutesForVirtualService(node, *virtualService,
		listenPort, meshGateway, opts)
	if err != nil || len(routes) == 0 {
		return nil
	}

	hosts, matchingRegistryServices := separateVSHostsAndServices(*virtualService, serviceRegistry, mostSpecificWildcardVsIndex)

	// Gateway allows only routes from the namespace of the proxy, or namespace of the destination.
	if model.UseGatewaySemantics(*virtualService) {
		res := make([]*model.Service, 0, len(matchingRegistryServices))
		for _, s := range matchingRegistryServices {
			if s.Attributes.Namespace != virtualService.Namespace && node.ConfigNamespace != virtualService.Namespace {
				continue
			}
			res = append(res, s)
		}
		if len(res) == 0 {
			return nil
		}
	}

	// Now group these Services by port so that we can infer the destination.port if the user
	// doesn't specify any port for a multiport service. We need to know the destination port in
	// order to build the cluster name (outbound|<port>|<subset>|<serviceFQDN>)
	// If the destination service is being accessed on port X, we set that as the default
	// destination port
	serviceByPort := make(map[int][]*model.Service)
	for _, svc := range matchingRegistryServices {
		for _, port := range svc.Ports {
			if port.Protocol.IsHTTPOrSniffed() {
				serviceByPort[port.Port] = append(serviceByPort[port.Port], svc)
			}
		}
	}

	if len(serviceByPort) == 0 {
		if listenPort == 80 {
			// TODO: This is a gross HACK. Fix me. Its a much bigger surgery though, due to the way
			// the current code is written.
			serviceByPort[80] = nil
		}
	}

	out := make([]VirtualHostWrapper, 0, len(serviceByPort))
	for port, services := range serviceByPort {
		out = append(out, VirtualHostWrapper{
			Port:                port,
			Services:            services,
			VirtualServiceHosts: hosts,
			Routes:              routes,
		})
	}

	return out
}

func buildSidecarVirtualHostForService(svc *model.Service,
	port *model.Port,
	hash *networking.LoadBalancerSettings_ConsistentHashLB,
	push *model.PushContext,
) VirtualHostWrapper {
	cluster := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", svc.Hostname, port.Port)
	traceOperation := telemetry.TraceOperation(string(svc.Hostname), port.Port)
	httpRoute := BuildDefaultHTTPOutboundRoute(cluster, traceOperation, push.Mesh)

	// if this host has no virtualservice, the consistentHash on its destinationRule will be useless
	hashPolicy := consistentHashToHashPolicy(hash)
	if hashPolicy != nil {
		httpRoute.GetRoute().HashPolicy = []*route.RouteAction_HashPolicy{hashPolicy}
	}
	return VirtualHostWrapper{
		Port:     port.Port,
		Services: []*model.Service{svc},
		Routes:   []*route.Route{httpRoute},
	}
}

// GetDestinationCluster generates a cluster name for the route. If the destination is invalid
// or cannot be found, "UnknownService" is returned.
func GetDestinationCluster(destination *networking.Destination, service *model.Service, listenerPort int) string {
	if len(destination.GetHost()) == 0 {
		// only happens when the gateway-api BackendRef is invalid
		return "UnknownService"
	}
	h := host.Name(destination.Host)
	// If this is an Alias, point to the concrete service
	// TODO: this will not work if we have Alias -> Alias -> Concrete service.
	if service != nil && service.Attributes.K8sAttributes.ExternalName != "" {
		h = host.Name(service.Attributes.K8sAttributes.ExternalName)
	}
	port := listenerPort
	if destination.GetPort() != nil {
		port = int(destination.GetPort().GetNumber())
	} else if service != nil && len(service.Ports) == 1 {
		// if service only has one port defined, use that as the port, otherwise use default listenerPort
		port = service.Ports[0].Port

		// Do not return blackhole cluster for service==nil case as there is a legitimate use case for
		// calling this function with nil service: to route to a pre-defined statically configured cluster
		// declared as part of the bootstrap.
		// If blackhole cluster is needed, do the check on the caller side. See gateway and tls.go for examples.
	}

	return model.BuildSubsetKey(model.TrafficDirectionOutbound, destination.Subset, h, port)
}

type RouteOptions struct {
	// IsTLS indicates if the route is intended for a TLS listener
	IsTLS bool
	// IsHTTP3AltSvcHeaderNeeded indicates if HTTP3 alt-svc header needs to be inserted
	IsHTTP3AltSvcHeaderNeeded bool
	Mesh                      *meshconfig.MeshConfig
	Push                      *model.PushContext
	LookupService             func(name host.Name) *model.Service
	LookupDestinationCluster  func(destination *networking.Destination, service *model.Service, listenerPort int) string
	LookupHash                func(*networking.HTTPRouteDestination) *networking.LoadBalancerSettings_ConsistentHashLB

	InferencePoolExtensionRefs map[string]kube.InferencePoolRouteRuleConfig
}

// BuildHTTPRoutesForVirtualService creates data plane HTTP routes from the virtual service spec.
// The rule should be adapted to destination names (outbound clusters).
// Each rule is guarded by source labels.
//
// This is called for each port to compute virtual hosts.
// Each VirtualService is tried, with a list of Services that listen on the port.
// Error indicates the given virtualService can't be used on the port.
// This function is used by both the gateway and the sidecar
func BuildHTTPRoutesForVirtualService(
	node *model.Proxy,
	virtualService config.Config,
	listenPort int,
	gatewayNames sets.String,
	opts RouteOptions,
) ([]*route.Route, error) {
	vs, ok := virtualService.Spec.(*networking.VirtualService)
	if !ok { // should never happen
		return nil, fmt.Errorf("in not a virtual service: %#v", virtualService)
	}

	out := make([]*route.Route, 0, len(vs.Http))

	catchall := false
	for _, http := range vs.Http {
		if len(http.Match) == 0 {
			if r := TranslateRoute(node, http, nil, listenPort, virtualService, gatewayNames, opts); r != nil {
				out = append(out, r)
			}
			catchall = true
		} else {
			for _, match := range http.Match {
				if r := TranslateRoute(node, http, match, listenPort, virtualService, gatewayNames, opts); r != nil {
					out = append(out, r)
					// This is a catch all path. Routes are matched in order, so we will never go beyond this match
					// As an optimization, we can just stop sending any more routes here.
					if IsCatchAllRoute(r) {
						catchall = true
						break
					}
				}
			}
		}
		if catchall {
			break
		}
	}

	if len(out) == 0 {
		return nil, fmt.Errorf("no routes matched")
	}
	return out, nil
}

// sourceMatchHttp checks if the sourceLabels or the gateways in a match condition match with the
// labels for the proxy or the gateway name for which we are generating a route
func sourceMatchHTTP(match *networking.HTTPMatchRequest, proxyLabels labels.Instance, gatewayNames sets.String, proxyNamespace string) bool {
	if match == nil {
		return true
	}

	// Trim by source labels or mesh gateway
	if len(match.Gateways) > 0 {
		for _, g := range match.Gateways {
			if gatewayNames.Contains(g) {
				return true
			}
		}
	} else if labels.Instance(match.GetSourceLabels()).SubsetOf(proxyLabels) {
		return match.SourceNamespace == "" || match.SourceNamespace == proxyNamespace
	}

	return false
}

// TranslateRoute translates HTTP routes
func TranslateRoute(
	node *model.Proxy,
	in *networking.HTTPRoute,
	match *networking.HTTPMatchRequest,
	listenPort int,
	virtualService config.Config,
	gatewayNames sets.String,
	opts RouteOptions,
) *route.Route {
	// When building routes, it's okay if the target cluster cannot be
	// resolved. Traffic to such clusters will blackhole.

	// Match by the destination port specified in the match condition
	if match != nil && match.Port != 0 && match.Port != uint32(listenPort) {
		return nil
	}
	// Match by source labels/gateway names inside the match condition
	if !sourceMatchHTTP(match, node.Labels, gatewayNames, node.Metadata.Namespace) {
		return nil
	}

	routeName := in.Name
	if match != nil && match.Name != "" {
		routeName = routeName + "." + match.Name
	}

	out := &route.Route{
		Name:     routeName,
		Match:    TranslateRouteMatch(virtualService, match),
		Metadata: util.BuildConfigInfoMetadata(virtualService.Meta),
	}

	if match != nil && match.StatPrefix != "" {
		out.StatPrefix = match.StatPrefix
	}

	authority := ""
	if in.Headers != nil {
		operations := TranslateHeadersOperations(in.Headers)
		out.RequestHeadersToAdd = operations.RequestHeadersToAdd
		out.ResponseHeadersToAdd = operations.ResponseHeadersToAdd
		out.RequestHeadersToRemove = operations.RequestHeadersToRemove
		out.ResponseHeadersToRemove = operations.ResponseHeadersToRemove
		authority = operations.Authority
	}

	var hostnames []host.Name
	if infPoolRouteRuleCfg, ok := opts.InferencePoolExtensionRefs[in.Name]; ok {
		// This route has an inference pool config, set up ext_proc
		extSvcHost := host.Name(infPoolRouteRuleCfg.FQDN)
		extPortNum, _ := strconv.Atoi(infPoolRouteRuleCfg.Port)
		if out.TypedPerFilterConfig == nil {
			out.TypedPerFilterConfig = make(map[string]*anypb.Any)
		}
		out.TypedPerFilterConfig[wellknown.HTTPExternalProcessing] = protoconv.MessageToAny(&extproc.ExtProcPerRoute{
			Override: &extproc.ExtProcPerRoute_Overrides{
				Overrides: &extproc.ExtProcOverrides{
					FailureModeAllow: &wrapperspb.BoolValue{Value: infPoolRouteRuleCfg.FailureModeAllow},
					GrpcService: &core.GrpcService{
						TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
							EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
								ClusterName: model.BuildSubsetKey(model.TrafficDirectionOutbound, "", extSvcHost, extPortNum),
							},
						},
					},
					ProcessingMode: &extproc.ProcessingMode{
						RequestHeaderMode: extproc.ProcessingMode_SEND,
						// open AI standard includes the model and other information the ext_proc server needs in the request body
						RequestBodyMode: extproc.ProcessingMode_FULL_DUPLEX_STREAMED,
						// If the ext_proc server has the request_body_mode set to FULL_DUPLEX_STREAMED, then the request_trailer_mode has to be set to SEND
						RequestTrailerMode: extproc.ProcessingMode_SEND,
						ResponseHeaderMode: extproc.ProcessingMode_SEND,
						// GIE collects statistics present in the open AI standard response message
						ResponseBodyMode: extproc.ProcessingMode_FULL_DUPLEX_STREAMED,
						// If the ext_proc server has the response_body_mode set to FULL_DUPLEX_STREAMED, then the response_trailer_mode has to be set to SEND
						ResponseTrailerMode: extproc.ProcessingMode_SEND,
					},
				},
			},
		})
	}
	if in.Redirect != nil {
		ApplyRedirect(out, in.Redirect, listenPort, opts.IsTLS, model.UseGatewaySemantics(virtualService))
	} else if in.DirectResponse != nil {
		ApplyDirectResponse(out, in.DirectResponse)
	} else {
		hostnames = applyHTTPRouteDestination(out, node, virtualService, in, opts, authority, listenPort)
	}

	out.Decorator = &route.Decorator{
		Operation: GetRouteOperation(out, virtualService.Name, listenPort),
	}
	if in.Fault != nil || in.CorsPolicy != nil {
		out.TypedPerFilterConfig = make(map[string]*anypb.Any)
	}
	if in.Fault != nil {
		out.TypedPerFilterConfig[wellknown.Fault] = protoconv.MessageToAny(TranslateFault(in.Fault))
	}
	if in.CorsPolicy != nil {
		out.TypedPerFilterConfig[wellknown.CORS] = protoconv.MessageToAny(TranslateCORSPolicy(node, in.CorsPolicy))
	}
	var statefulConfig *statefulsession.StatefulSession
	for _, hostname := range hostnames {
		svc := opts.LookupService(hostname)
		perSvcStatefulConfig := util.MaybeBuildStatefulSessionFilterConfig(svc)
		// This means we have more than one stateful config for the same route because of weighed destinations.
		// We should just pick the first and give a warning.
		if perSvcStatefulConfig != nil && statefulConfig != nil {
			log.Warnf("More than one stateful config for the same route %s. Picking the first one.", routeName)
			break
		}
		statefulConfig = perSvcStatefulConfig
	}
	// Build stateful set config if the svc has appropriate labels attached.
	if statefulConfig != nil {
		if out.TypedPerFilterConfig == nil {
			out.TypedPerFilterConfig = make(map[string]*anypb.Any)
		}
		perRouteStatefulSession := &statefulsession.StatefulSessionPerRoute{
			Override: &statefulsession.StatefulSessionPerRoute_StatefulSession{
				StatefulSession: statefulConfig,
			},
		}
		out.TypedPerFilterConfig[util.StatefulSessionFilter] = protoconv.MessageToAny(perRouteStatefulSession)
	}

	if opts.IsHTTP3AltSvcHeaderNeeded {
		http3AltSvcHeader := buildHTTP3AltSvcHeader(listenPort, util.ALPNHttp3OverQUIC)
		if out.ResponseHeadersToAdd == nil {
			out.ResponseHeadersToAdd = make([]*core.HeaderValueOption, 0)
		}
		out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, http3AltSvcHeader)
	}

	return out
}

func applyHTTPRouteDestination(
	out *route.Route,
	node *model.Proxy,
	vs config.Config,
	in *networking.HTTPRoute,
	opts RouteOptions,
	authority string,
	listenerPort int,
) []host.Name {
	action := &route.RouteAction{}

	setTimeout(action, in.Timeout, node)

	if model.UseGatewaySemantics(vs) {
		// return 500 for invalid backends
		// https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L204
		action.ClusterNotFoundResponseCode = route.RouteAction_INTERNAL_SERVER_ERROR
	}

	out.Action = &route.Route_Route{Route: action}

	if in.Rewrite != nil {
		ph := util.GetProxyHeaders(node, opts.Push, istionetworking.ListenerClassSidecarOutbound)
		appendXForwardedHost := ph.XForwardedHost
		action.ClusterSpecifier = &route.RouteAction_Cluster{
			Cluster: in.Name,
		}
		action.AppendXForwardedHost = appendXForwardedHost

		if regexRewrite := in.Rewrite.GetUriRegexRewrite(); regexRewrite != nil {
			action.RegexRewrite = &matcher.RegexMatchAndSubstitute{
				Pattern: &matcher.RegexMatcher{
					Regex: regexRewrite.Match,
				},
				Substitution: regexRewrite.Rewrite,
			}
		} else if uri := in.Rewrite.GetUri(); uri != "" {
			if model.UseGatewaySemantics(vs) && uri == "/" {
				// remove the prefix
				action.RegexRewrite = &matcher.RegexMatchAndSubstitute{
					Pattern: &matcher.RegexMatcher{
						Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(out.Match.GetPathSeparatedPrefix())),
					},
					// hold `/` in case the entire path is removed
					Substitution: `/\2`,
				}
			} else {
				action.PrefixRewrite = uri
			}
		}
		if in.Rewrite.GetAuthority() != "" {
			authority = in.Rewrite.GetAuthority()
		}
	}
	if authority != "" {
		action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{
			HostRewriteLiteral: authority,
		}
	}

	if in.Mirror != nil {
		if mp := MirrorPercent(in); mp != nil {
			cluster := opts.LookupDestinationCluster(in.Mirror, opts.LookupService(host.Name(in.Mirror.Host)), listenerPort)
			action.RequestMirrorPolicies = append(action.RequestMirrorPolicies,
				TranslateRequestMirrorPolicy(cluster, mp))
		}
	}
	for _, mirror := range in.Mirrors {
		if mp := MirrorPercentByPolicy(mirror); mp != nil && mirror.Destination != nil {
			cluster := opts.LookupDestinationCluster(mirror.Destination, opts.LookupService(host.Name(mirror.Destination.Host)), listenerPort)
			action.RequestMirrorPolicies = append(action.RequestMirrorPolicies,
				TranslateRequestMirrorPolicy(cluster, mp))
		}
	}

	var hostnames []host.Name
	policy := in.Retries
	if policy == nil {
		// No VS policy set, use mesh defaults
		policy = opts.Mesh.GetDefaultHttpRetryPolicy()
	}
	consistentHash := false
	if len(in.Route) == 1 {
		hostnames = append(hostnames, processDestination(in.Route[0], opts, listenerPort, out, action))
		hash := opts.LookupHash(in.Route[0])
		consistentHash = hash != nil
	} else {
		weighted := make([]*route.WeightedCluster_ClusterWeight, 0)
		for _, dst := range in.Route {
			if dst.Weight == 0 {
				// Ignore 0 weighted clusters if there are other clusters in the route.
				continue
			}
			destinationweight, hostname := processWeightedDestination(dst, opts, listenerPort, action)
			weighted = append(weighted, destinationweight)
			hostnames = append(hostnames, hostname)
		}
		action.ClusterSpecifier = &route.RouteAction_WeightedClusters{
			WeightedClusters: &route.WeightedCluster{
				Clusters: weighted,
			},
		}
	}
	action.RetryPolicy = retry.ConvertPolicy(policy, consistentHash)
	return hostnames
}

// processDestination processes a single destination in a route. It specifies to which cluster the route should
// be routed to. It also sets the headers and hash policy if specified.
// Returns the hostname of the destination.
func processDestination(dst *networking.HTTPRouteDestination, opts RouteOptions, listenerPort int, out *route.Route, action *route.RouteAction) host.Name {
	hostname := host.Name(dst.GetDestination().GetHost())
	action.ClusterSpecifier = &route.RouteAction_Cluster{
		Cluster: opts.LookupDestinationCluster(dst.Destination, opts.LookupService(hostname), listenerPort),
	}
	if dst.Headers != nil {
		operations := TranslateHeadersOperations(dst.Headers)
		out.RequestHeadersToAdd = append(out.RequestHeadersToAdd, operations.RequestHeadersToAdd...)
		out.RequestHeadersToRemove = append(out.RequestHeadersToRemove, operations.RequestHeadersToRemove...)
		out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, operations.ResponseHeadersToAdd...)
		out.ResponseHeadersToRemove = append(out.ResponseHeadersToRemove, operations.ResponseHeadersToRemove...)
		if operations.Authority != "" && action.HostRewriteSpecifier == nil {
			// Ideally, if the weighted cluster overwrites authority, it has precedence. This mirrors behavior of headers,
			// because for headers we append the weighted last which allows it to Set and wipe out previous Adds.
			// However, Envoy behavior is different when we set at both cluster level and route level, and we want
			// behavior to be consistent with a single cluster and multiple clusters.
			// As a result, we only override if the top level rewrite is not set
			action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{
				HostRewriteLiteral: operations.Authority,
			}
		}
	}
	hash := opts.LookupHash(dst)
	hashPolicy := consistentHashToHashPolicy(hash)
	if hashPolicy != nil {
		action.HashPolicy = append(action.HashPolicy, hashPolicy)
	}
	return hostname
}

// processWeightedDestination processes a weighted destination in a route. It specifies to which cluster the route should
// be routed to. It also sets the headers and hash policy if specified.
// Returns the hostname of the destination along with its weight.
func processWeightedDestination(
	dst *networking.HTTPRouteDestination,
	opts RouteOptions,
	listenerPort int,
	action *route.RouteAction,
) (*route.WeightedCluster_ClusterWeight, host.Name) {
	hostname := host.Name(dst.GetDestination().GetHost())
	clusterWeight := &route.WeightedCluster_ClusterWeight{
		Name:   opts.LookupDestinationCluster(dst.Destination, opts.LookupService(hostname), listenerPort),
		Weight: &wrapperspb.UInt32Value{Value: uint32(dst.Weight)},
	}
	if dst.Headers != nil {
		operations := TranslateHeadersOperations(dst.Headers)
		// If weighted destination has headers, we need to set them on the cluster weight.
		clusterWeight.RequestHeadersToAdd = operations.RequestHeadersToAdd
		clusterWeight.RequestHeadersToRemove = operations.RequestHeadersToRemove
		clusterWeight.ResponseHeadersToAdd = operations.ResponseHeadersToAdd
		clusterWeight.ResponseHeadersToRemove = operations.ResponseHeadersToRemove
		if operations.Authority != "" {
			clusterWeight.HostRewriteSpecifier = &route.WeightedCluster_ClusterWeight_HostRewriteLiteral{
				HostRewriteLiteral: operations.Authority,
			}
		}
	}
	hash := opts.LookupHash(dst)
	hashPolicy := consistentHashToHashPolicy(hash)
	if hashPolicy != nil {
		action.HashPolicy = append(action.HashPolicy, hashPolicy)
	}
	return clusterWeight, hostname
}

func ApplyRedirect(out *route.Route, redirect *networking.HTTPRedirect, port int, isTLS bool, useGatewaySemantics bool) {
	action := &route.Route_Redirect{
		Redirect: &route.RedirectAction{
			HostRedirect: redirect.Authority,
			PathRewriteSpecifier: &route.RedirectAction_PathRedirect{
				PathRedirect: redirect.Uri,
			},
		},
	}

	if useGatewaySemantics {
		if uri, isPrefixReplace := cutPrefix(redirect.Uri, "%PREFIX()%"); isPrefixReplace {
			action.Redirect.PathRewriteSpecifier = &route.RedirectAction_PrefixRewrite{
				PrefixRewrite: uri,
			}
		}
	}

	if redirect.Scheme != "" {
		action.Redirect.SchemeRewriteSpecifier = &route.RedirectAction_SchemeRedirect{SchemeRedirect: redirect.Scheme}
	}

	if redirect.RedirectPort != nil {
		switch rp := redirect.RedirectPort.(type) {
		case *networking.HTTPRedirect_DerivePort:
			if rp.DerivePort == networking.HTTPRedirect_FROM_REQUEST_PORT {
				// Envoy doesn't actually support deriving the port from the request dynamically. However,
				// we always generate routes in the context of a specific request port. As a result, we can just
				// use that port
				action.Redirect.PortRedirect = uint32(port)
			}
			// Otherwise, no port needed; HTTPRedirect_FROM_PROTOCOL_DEFAULT is Envoy's default behavior
		case *networking.HTTPRedirect_Port:
			action.Redirect.PortRedirect = rp.Port
		}
		scheme := redirect.Scheme
		if scheme == "" {
			if isTLS {
				scheme = "https"
			} else {
				scheme = "http"
			}
		}
		// Do not put explicit :80 or :443 when its http/https
		if action.Redirect.PortRedirect == 80 && scheme == "http" {
			action.Redirect.PortRedirect = 0
		}
		if action.Redirect.PortRedirect == 443 && scheme == "https" {
			action.Redirect.PortRedirect = 0
		}
	}

	switch redirect.RedirectCode {
	case 0, 301:
		action.Redirect.ResponseCode = route.RedirectAction_MOVED_PERMANENTLY
	case 302:
		action.Redirect.ResponseCode = route.RedirectAction_FOUND
	case 303:
		action.Redirect.ResponseCode = route.RedirectAction_SEE_OTHER
	case 307:
		action.Redirect.ResponseCode = route.RedirectAction_TEMPORARY_REDIRECT
	case 308:
		action.Redirect.ResponseCode = route.RedirectAction_PERMANENT_REDIRECT
	default:
		log.Warnf("Redirect Code %d is not yet supported", redirect.RedirectCode)
		// Can't just set action to nil here because the proto marshaller will still see
		// the Route_Redirect type of the variable and assume that the value is set
		// (and panic because it's not). What we need to do is set out.Action directly to
		// (a typeless) nil so that type assertions to Route_Redirect will fail.
		out.Action = nil
		return
	}

	out.Action = action
}

func ApplyDirectResponse(out *route.Route, directResponse *networking.HTTPDirectResponse) {
	action := &route.Route_DirectResponse{
		DirectResponse: &route.DirectResponseAction{
			Status: directResponse.Status,
		},
	}

	if directResponse.Body != nil {
		switch op := directResponse.Body.Specifier.(type) {
		case *networking.HTTPBody_String_:
			action.DirectResponse.Body = &core.DataSource{
				Specifier: &core.DataSource_InlineString{
					InlineString: op.String_,
				},
			}
		case *networking.HTTPBody_Bytes:
			action.DirectResponse.Body = &core.DataSource{
				Specifier: &core.DataSource_InlineBytes{
					InlineBytes: op.Bytes,
				},
			}
		}
	}

	out.Action = action
}

func buildHTTP3AltSvcHeader(port int, h3Alpns []string) *core.HeaderValueOption {
	// For example, www.cloudflare.com returns the following
	// alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400
	valParts := make([]string, 0, len(h3Alpns))
	for _, alpn := range h3Alpns {
		// Max-age is hardcoded to 1 day for now.
		valParts = append(valParts, fmt.Sprintf(`%s=":%d"; ma=86400`, alpn, port))
	}
	headerVal := strings.Join(valParts, ", ")
	return &core.HeaderValueOption{
		AppendAction: core.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD,
		Header: &core.HeaderValue{
			Key:   util.AltSvcHeader,
			Value: headerVal,
		},
	}
}

// SortHeaderValueOption type and the functions below (Len, Less and Swap) are for sort.Stable for type HeaderValueOption
type SortHeaderValueOption []*core.HeaderValueOption

// MirrorPercent computes the mirror percent to be used based on "Mirror" data in route.
func MirrorPercent(in *networking.HTTPRoute) *core.RuntimeFractionalPercent {
	switch {
	case in.MirrorPercentage != nil:
		if in.MirrorPercentage.GetValue() > 0 {
			return &core.RuntimeFractionalPercent{
				DefaultValue: translatePercentToFractionalPercent(in.MirrorPercentage),
			}
		}
		// If zero percent is provided explicitly, we should not mirror.
		return nil
	// nolint: staticcheck
	case in.MirrorPercent != nil:
		if in.MirrorPercent.GetValue() > 0 {
			return &core.RuntimeFractionalPercent{
				DefaultValue: translateIntegerToFractionalPercent((int32(in.MirrorPercent.GetValue()))),
			}
		}
		// If zero percent is provided explicitly, we should not mirror.
		return nil
	default:
		// Default to 100 percent if percent is not given.
		return &core.RuntimeFractionalPercent{
			DefaultValue: translateIntegerToFractionalPercent(100),
		}
	}
}

// MirrorPercentByPolicy computes the mirror percent to be used based on HTTPMirrorPolicy.
func MirrorPercentByPolicy(mirror *networking.HTTPMirrorPolicy) *core.RuntimeFractionalPercent {
	switch {
	case mirror.Percentage != nil:
		if mirror.Percentage.GetValue() > 0 {
			return &core.RuntimeFractionalPercent{
				DefaultValue: translatePercentToFractionalPercent(mirror.Percentage),
			}
		}
		// If zero percent is provided explicitly, we should not mirror.
		return nil
	default:
		// Default to 100 percent if percent is not given.
		return &core.RuntimeFractionalPercent{
			DefaultValue: translateIntegerToFractionalPercent(100),
		}
	}
}

// Len is i the sort.Interface for SortHeaderValueOption
func (b SortHeaderValueOption) Len() int {
	return len(b)
}

// Less is in the sort.Interface for SortHeaderValueOption
func (b SortHeaderValueOption) Less(i, j int) bool {
	if b[i] == nil || b[i].Header == nil {
		return false
	} else if b[j] == nil || b[j].Header == nil {
		return true
	}
	return strings.Compare(b[i].Header.Key, b[j].Header.Key) < 0
}

// Swap is in the sort.Interface for SortHeaderValueOption
func (b SortHeaderValueOption) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

// translateAppendHeaders translates headers
func translateAppendHeaders(headers map[string]string, appendFlag bool) ([]*core.HeaderValueOption, string) {
	if len(headers) == 0 {
		return nil, ""
	}
	authority := ""
	headerValueOptionList := make([]*core.HeaderValueOption, 0, len(headers))
	for key, value := range headers {
		if isAuthorityHeader(key) {
			// If there are multiple, last one wins; validation will reject
			authority = value
		}
		if isInternalHeader(key) {
			continue
		}
		headerValueOption := &core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   key,
				Value: value,
			},
		}
		if appendFlag {
			headerValueOption.AppendAction = core.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD
		} else {
			headerValueOption.AppendAction = core.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD
		}
		headerValueOptionList = append(headerValueOptionList, headerValueOption)
	}
	sort.Stable(SortHeaderValueOption(headerValueOptionList))
	return headerValueOptionList, authority
}

type HeadersOperations struct {
	RequestHeadersToAdd     []*core.HeaderValueOption
	ResponseHeadersToAdd    []*core.HeaderValueOption
	RequestHeadersToRemove  []string
	ResponseHeadersToRemove []string
	Authority               string
}

// isInternalHeader returns true if a header refers to an internal value that cannot be modified by Envoy
func isInternalHeader(headerKey string) bool {
	return strings.HasPrefix(headerKey, ":") || strings.EqualFold(headerKey, "host")
}

// isAuthorityHeader returns true if a header refers to the authority header
func isAuthorityHeader(headerKey string) bool {
	return strings.EqualFold(headerKey, ":authority") || strings.EqualFold(headerKey, "host")
}

func dropInternal(keys []string) []string {
	result := make([]string, 0, len(keys))
	for _, k := range keys {
		if isInternalHeader(k) {
			continue
		}
		result = append(result, k)
	}
	return result
}

// TranslateHeadersOperations translates headers operations
func TranslateHeadersOperations(headers *networking.Headers) HeadersOperations {
	req := headers.GetRequest()
	resp := headers.GetResponse()

	requestHeadersToAdd, setAuthority := translateAppendHeaders(req.GetSet(), false)
	reqAdd, addAuthority := translateAppendHeaders(req.GetAdd(), true)
	requestHeadersToAdd = append(requestHeadersToAdd, reqAdd...)

	responseHeadersToAdd, _ := translateAppendHeaders(resp.GetSet(), false)
	respAdd, _ := translateAppendHeaders(resp.GetAdd(), true)
	responseHeadersToAdd = append(responseHeadersToAdd, respAdd...)

	auth := addAuthority
	if setAuthority != "" {
		// If authority is set in 'add' and 'set', pick the one from 'set'
		auth = setAuthority
	}
	return HeadersOperations{
		RequestHeadersToAdd:     requestHeadersToAdd,
		ResponseHeadersToAdd:    responseHeadersToAdd,
		RequestHeadersToRemove:  dropInternal(req.GetRemove()),
		ResponseHeadersToRemove: dropInternal(resp.GetRemove()),
		Authority:               auth,
	}
}

// TranslateRouteMatch translates match condition
func TranslateRouteMatch(vs config.Config, in *networking.HTTPMatchRequest) *route.RouteMatch {
	out := &route.RouteMatch{PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}}
	if in == nil {
		return out
	}

	for name, stringMatch := range in.Headers {
		// The metadata matcher takes precedence over the header matcher.
		if metadataMatcher := translateMetadataMatch(name, stringMatch); metadataMatcher != nil {
			out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher)
		} else {
			matcher := translateHeaderMatch(name, stringMatch)
			out.Headers = append(out.Headers, matcher)
		}
	}

	for name, stringMatch := range in.WithoutHeaders {
		if metadataMatcher := translateMetadataMatch(name, stringMatch); metadataMatcher != nil {
			metadataMatcher.Invert = true
			out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher)
		} else {
			matcher := translateHeaderMatch(name, stringMatch)
			matcher.InvertMatch = true
			// treat_missing_header_as_empty conflict with present_match
			if !canBeConvertedToPresentMatch(stringMatch) {
				matcher.TreatMissingHeaderAsEmpty = true
			}
			out.Headers = append(out.Headers, matcher)
		}
	}

	// guarantee ordering of headers
	sort.Slice(out.Headers, func(i, j int) bool {
		return out.Headers[i].Name < out.Headers[j].Name
	})

	if in.Uri != nil {
		switch m := in.Uri.MatchType.(type) {
		case *networking.StringMatch_Exact:
			out.PathSpecifier = &route.RouteMatch_Path{Path: m.Exact}
		case *networking.StringMatch_Prefix:
			if (model.UseIngressSemantics(vs) || model.UseGatewaySemantics(vs)) && m.Prefix != "/" {
				path := strings.TrimSuffix(m.Prefix, "/")
				out.PathSpecifier = &route.RouteMatch_PathSeparatedPrefix{PathSeparatedPrefix: path}
			} else {
				out.PathSpecifier = &route.RouteMatch_Prefix{Prefix: m.Prefix}
			}
		case *networking.StringMatch_Regex:
			out.PathSpecifier = &route.RouteMatch_SafeRegex{
				SafeRegex: &matcher.RegexMatcher{
					Regex: m.Regex,
				},
			}
		}
	}

	out.CaseSensitive = &wrapperspb.BoolValue{Value: !in.IgnoreUriCase}

	if in.Method != nil {
		matcher := translateHeaderMatch(HeaderMethod, in.Method)
		out.Headers = append(out.Headers, matcher)
	}

	if in.Authority != nil {
		matcher := translateHeaderMatch(HeaderAuthority, in.Authority)
		out.Headers = append(out.Headers, matcher)
	}

	if in.Scheme != nil {
		matcher := translateHeaderMatch(HeaderScheme, in.Scheme)
		out.Headers = append(out.Headers, matcher)
	}

	for name, stringMatch := range in.QueryParams {
		matcher := translateQueryParamMatch(name, stringMatch)
		out.QueryParameters = append(out.QueryParameters, matcher)
	}

	return out
}

// translateQueryParamMatch translates a StringMatch to a QueryParameterMatcher.
func translateQueryParamMatch(name string, in *networking.StringMatch) *route.QueryParameterMatcher {
	out := &route.QueryParameterMatcher{
		Name: name,
	}

	if canBeConvertedToPresentMatch(in) {
		out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_PresentMatch{
			PresentMatch: true,
		}
		return out
	}

	if em := util.ConvertToEnvoyMatch(in); em != nil {
		out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{
			StringMatch: em,
		}
	}

	return out
}

// canBeConvertedToPresentMatch determines if the given matcher can be converted to present_match or not.
// Currently, if the regex is "*" value, it returns true
func canBeConvertedToPresentMatch(in *networking.StringMatch) bool {
	if in == nil || in.MatchType == nil {
		return true
	}

	catchall := false

	switch m := in.MatchType.(type) {
	case *networking.StringMatch_Regex:
		// `*` is NOT a RE2 style regex, it's a metacharacter.
		// It will be translated as present_match, rather than matching "any string".
		// see https://github.com/istio/istio/pull/20629
		catchall = m.Regex == "*"
	}

	return catchall
}

// translateMetadataMatch translates a header match to dynamic metadata matcher. Returns nil if the header is not supported
// or the header format is invalid for generating metadata matcher.
//
// The currently only supported header is @request.auth.claims for JWT claims matching. Claims of type string or list of string
// are supported and nested claims are also supported using `.` or `[]` as a separator for claim names, `[]` is recommended.
//
// Examples using `.` as a separator:
// - `@request.auth.claims.admin` matches the claim "admin".
// - `@request.auth.claims.group.id` matches the nested claims "group" and "id".
//
// Examples using `[]` as a separator:
// - `@request.auth.claims[admin]` matches the claim "admin".
// - `@request.auth.claims[group][id]` matches the nested claims "group" and "id".
func translateMetadataMatch(name string, in *networking.StringMatch) *matcher.MetadataMatcher {
	rc := jwt.ToRoutingClaim(name)
	if !rc.Match {
		return nil
	}
	return authz.MetadataMatcherForJWTClaims(rc.Claims, util.ConvertToEnvoyMatch(in))
}

// translateHeaderMatch translates to HeaderMatcher
func translateHeaderMatch(name string, in *networking.StringMatch) *route.HeaderMatcher {
	out := &route.HeaderMatcher{
		Name: name,
	}

	if canBeConvertedToPresentMatch(in) {
		out.HeaderMatchSpecifier = &route.HeaderMatcher_PresentMatch{PresentMatch: true}
		return out
	}

	if em := util.ConvertToEnvoyMatch(in); em != nil {
		out.HeaderMatchSpecifier = &route.HeaderMatcher_StringMatch{
			StringMatch: em,
		}
	}

	return out
}

func forwardNotMatchingPreflights(cors *networking.CorsPolicy) *wrapperspb.BoolValue {
	if cors.GetUnmatchedPreflights() == networking.CorsPolicy_IGNORE {
		return wrapperspb.Bool(false)
	}

	// This is the default behavior before envoy 1.30.
	return wrapperspb.Bool(true)
}

// TranslateCORSPolicy translates CORS policy
func TranslateCORSPolicy(proxy *model.Proxy, in *networking.CorsPolicy) *cors.CorsPolicy {
	if in == nil {
		return nil
	}

	// CORS filter is enabled by default
	out := cors.CorsPolicy{}

	out.ForwardNotMatchingPreflights = forwardNotMatchingPreflights(in)

	// nolint: staticcheck
	if in.AllowOrigins != nil {
		out.AllowOriginStringMatch = util.ConvertToEnvoyMatches(in.AllowOrigins)
	} else if in.AllowOrigin != nil {
		out.AllowOriginStringMatch = util.StringToExactMatch(in.AllowOrigin)
	}

	out.FilterEnabled = &core.RuntimeFractionalPercent{
		DefaultValue: &xdstype.FractionalPercent{
			Numerator:   100,
			Denominator: xdstype.FractionalPercent_HUNDRED,
		},
	}

	out.AllowCredentials = in.AllowCredentials
	out.AllowHeaders = strings.Join(in.AllowHeaders, ",")
	out.AllowMethods = strings.Join(in.AllowMethods, ",")
	out.ExposeHeaders = strings.Join(in.ExposeHeaders, ",")
	if in.MaxAge != nil {
		out.MaxAge = strconv.FormatInt(in.MaxAge.GetSeconds(), 10)
	}
	return &out
}

// GetRouteOperation returns readable route description for trace.
func GetRouteOperation(in *route.Route, vsName string, port int) string {
	path := "/*"
	m := in.GetMatch()
	ps := m.GetPathSpecifier()
	if ps != nil {
		switch ps.(type) {
		case *route.RouteMatch_Prefix:
			path = m.GetPrefix() + "*"
		case *route.RouteMatch_Path:
			path = m.GetPath()
		case *route.RouteMatch_SafeRegex:
			path = m.GetSafeRegex().GetRegex()
		}
	}

	// If there is only one destination cluster in route, return host:port/uri as description of route.
	// Otherwise there are multiple destination clusters and destination host is not clear. For that case
	// return virtual service name:port/uri as substitute.
	if c := in.GetRoute().GetCluster(); model.IsValidSubsetKey(c) {
		// Parse host and port from cluster name.
		_, _, h, p := model.ParseSubsetKey(c)
		return string(h) + ":" + strconv.Itoa(p) + path
	}
	return vsName + ":" + strconv.Itoa(port) + path
}

// BuildDefaultHTTPInboundRoute builds a default inbound route.
func BuildDefaultHTTPInboundRoute(proxy *model.Proxy, clusterName string, operation string, protocol protocol.Instance) *route.Route {
	out := buildDefaultHTTPRoute(clusterName, operation)
	// For inbound, configure with notimeout.
	out.GetRoute().Timeout = Notimeout
	out.GetRoute().MaxStreamDuration = &route.RouteAction_MaxStreamDuration{
		MaxStreamDuration: Notimeout,
		// If not configured at all, the grpc-timeout header is not used and
		// gRPC requests time out like any other requests using timeout or its default.
		GrpcTimeoutHeaderMax: Notimeout,
	}
	// "reset-before-request" does not work well for gRPC streaming services.
	if !protocol.IsGRPC() {
		out.GetRoute().RetryPolicy = &route.RetryPolicy{
			RetryOn: "reset-before-request",
			NumRetries: &wrapperspb.UInt32Value{
				Value: 2,
			},
		}
	}
	return out
}

func buildDefaultHTTPRoute(clusterName string, operation string) *route.Route {
	routeAction := &route.RouteAction{
		ClusterSpecifier: &route.RouteAction_Cluster{Cluster: clusterName},
	}
	val := &route.Route{
		Match: TranslateRouteMatch(config.Config{}, nil),
		Decorator: &route.Decorator{
			Operation: operation,
		},
		Action: &route.Route_Route{
			Route: routeAction,
		},
	}

	val.Name = DefaultRouteName
	return val
}

// setTimeout sets timeout for a route.
func setTimeout(action *route.RouteAction, vsTimeout *durationpb.Duration, node *model.Proxy) {
	// Configure timeouts specified by Virtual Service if they are provided, otherwise set it to defaults.
	action.Timeout = Notimeout
	if vsTimeout != nil {
		action.Timeout = vsTimeout
	}
	if node != nil && node.IsProxylessGrpc() {
		// TODO(stevenctl) merge these paths; grpc's xDS impl will not read the deprecated value
		action.MaxStreamDuration = &route.RouteAction_MaxStreamDuration{
			MaxStreamDuration: action.Timeout,
		}
	} else {
		// If not configured at all, the grpc-timeout header is not used and
		// gRPC requests time out like any other requests using timeout or its default.
		// Use deprecated value for now as the replacement MaxStreamDuration has some regressions.
		// nolint: staticcheck
		if action.Timeout.AsDuration().Nanoseconds() == 0 {
			action.MaxGrpcTimeout = Notimeout
		} else {
			action.MaxGrpcTimeout = action.Timeout
		}
	}
}

// BuildDefaultHTTPOutboundRoute builds a default outbound route, including a retry policy.
func BuildDefaultHTTPOutboundRoute(clusterName string, operation string, mesh *meshconfig.MeshConfig) *route.Route {
	out := buildDefaultHTTPRoute(clusterName, operation)
	// Add a default retry policy for outbound routes.
	out.GetRoute().RetryPolicy = retry.ConvertPolicy(mesh.GetDefaultHttpRetryPolicy(), false)
	setTimeout(out.GetRoute(), nil, nil)
	return out
}

// translatePercentToFractionalPercent translates an v1alpha3 Percent instance
// to an envoy.type.FractionalPercent instance.
func translatePercentToFractionalPercent(p *networking.Percent) *xdstype.FractionalPercent {
	return &xdstype.FractionalPercent{
		Numerator:   uint32(p.Value * 10000),
		Denominator: xdstype.FractionalPercent_MILLION,
	}
}

// translateIntegerToFractionalPercent translates an int32 instance to an
// envoy.type.FractionalPercent instance.
func translateIntegerToFractionalPercent(p int32) *xdstype.FractionalPercent {
	return &xdstype.FractionalPercent{
		Numerator:   uint32(p),
		Denominator: xdstype.FractionalPercent_HUNDRED,
	}
}

// TranslateFault translates networking.HTTPFaultInjection into Envoy's HTTPFault
func TranslateFault(in *networking.HTTPFaultInjection) *xdshttpfault.HTTPFault {
	if in == nil {
		return nil
	}

	out := xdshttpfault.HTTPFault{}
	if in.Delay != nil {
		out.Delay = &xdsfault.FaultDelay{}
		if in.Delay.Percentage != nil {
			out.Delay.Percentage = translatePercentToFractionalPercent(in.Delay.Percentage)
		} else {
			out.Delay.Percentage = translateIntegerToFractionalPercent(in.Delay.Percent) // nolint: staticcheck
		}
		switch d := in.Delay.HttpDelayType.(type) {
		case *networking.HTTPFaultInjection_Delay_FixedDelay:
			out.Delay.FaultDelaySecifier = &xdsfault.FaultDelay_FixedDelay{
				FixedDelay: d.FixedDelay,
			}
		default:
			log.Warnf("Exponential faults are not yet supported")
			out.Delay = nil
		}
	}

	if in.Abort != nil {
		out.Abort = &xdshttpfault.FaultAbort{}
		if in.Abort.Percentage != nil {
			out.Abort.Percentage = translatePercentToFractionalPercent(in.Abort.Percentage)
		}
		switch a := in.Abort.ErrorType.(type) {
		case *networking.HTTPFaultInjection_Abort_HttpStatus:
			out.Abort.ErrorType = &xdshttpfault.FaultAbort_HttpStatus{
				HttpStatus: uint32(a.HttpStatus),
			}
		case *networking.HTTPFaultInjection_Abort_GrpcStatus:
			// We wouldn't have an unknown gRPC code here. This is because
			// the validation webhook would have already caught the invalid
			// code and we wouldn't reach here.
			out.Abort.ErrorType = &xdshttpfault.FaultAbort_GrpcStatus{
				GrpcStatus: uint32(grpc.SupportedGRPCStatus[a.GrpcStatus]),
			}
		default:
			log.Warnf("Only HTTP and gRPC type abort faults are supported")
			out.Abort = nil
		}
	}

	if out.Delay == nil && out.Abort == nil {
		return nil
	}

	return &out
}

func TranslateRequestMirrorPolicy(cluster string, mp *core.RuntimeFractionalPercent,
) *route.RouteAction_RequestMirrorPolicy {
	return &route.RouteAction_RequestMirrorPolicy{
		Cluster:                       cluster,
		RuntimeFraction:               mp,
		TraceSampled:                  &wrapperspb.BoolValue{Value: false},
		DisableShadowHostSuffixAppend: features.DisableShadowHostSuffix,
	}
}

func portLevelSettingsConsistentHash(dst *networking.Destination,
	pls []*networking.TrafficPolicy_PortTrafficPolicy,
) *networking.LoadBalancerSettings_ConsistentHashLB {
	if dst.Port != nil {
		portNumber := dst.GetPort().GetNumber()
		for _, setting := range pls {
			number := setting.GetPort().GetNumber()
			if number == portNumber {
				return setting.GetLoadBalancer().GetConsistentHash()
			}
		}
	}

	return nil
}

func consistentHashToHashPolicy(consistentHash *networking.LoadBalancerSettings_ConsistentHashLB) *route.RouteAction_HashPolicy {
	switch consistentHash.GetHashKey().(type) {
	case *networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName:
		return &route.RouteAction_HashPolicy{
			PolicySpecifier: &route.RouteAction_HashPolicy_Header_{
				Header: &route.RouteAction_HashPolicy_Header{
					HeaderName: consistentHash.GetHttpHeaderName(),
				},
			},
		}
	case *networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie:
		cookie := consistentHash.GetHttpCookie()
		var ttl *durationpb.Duration
		if cookie.GetTtl() != nil {
			ttl = cookie.GetTtl()
		}
		cookiePolicy := &route.RouteAction_HashPolicy_Cookie{
			Name: cookie.GetName(),
			Ttl:  ttl,
			Path: cookie.GetPath(),
		}
		if len(cookie.GetAttributes()) > 0 {
			attributes := make([]*route.RouteAction_HashPolicy_CookieAttribute, len(cookie.GetAttributes()))
			for i, attr := range cookie.GetAttributes() {
				attributes[i] = &route.RouteAction_HashPolicy_CookieAttribute{
					Name:  attr.GetName(),
					Value: attr.GetValue(),
				}
			}
			cookiePolicy.Attributes = attributes
		}

		return &route.RouteAction_HashPolicy{
			PolicySpecifier: &route.RouteAction_HashPolicy_Cookie_{
				Cookie: cookiePolicy,
			},
		}
	case *networking.LoadBalancerSettings_ConsistentHashLB_UseSourceIp:
		return &route.RouteAction_HashPolicy{
			PolicySpecifier: &route.RouteAction_HashPolicy_ConnectionProperties_{
				ConnectionProperties: &route.RouteAction_HashPolicy_ConnectionProperties{
					SourceIp: consistentHash.GetUseSourceIp(),
				},
			},
		}
	case *networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName:
		return &route.RouteAction_HashPolicy{
			PolicySpecifier: &route.RouteAction_HashPolicy_QueryParameter_{
				QueryParameter: &route.RouteAction_HashPolicy_QueryParameter{
					Name: consistentHash.GetHttpQueryParameterName(),
				},
			},
		}
	}
	return nil
}

func hashForService(push *model.PushContext,
	node *model.Proxy,
	svc *model.Service,
	port *model.Port,
) (*networking.LoadBalancerSettings_ConsistentHashLB, *model.ConsolidatedDestRule) {
	if push == nil {
		return nil, nil
	}
	mergedDR := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, svc.Hostname)
	destinationRule := mergedDR.GetRule()
	if destinationRule == nil {
		return nil, nil
	}
	rule := destinationRule.Spec.(*networking.DestinationRule)
	consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash()
	portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings()
	for _, setting := range portLevelSettings {
		number := setting.GetPort().GetNumber()
		if int(number) == port.Port {
			if setting.GetLoadBalancer().GetConsistentHash() != nil {
				consistentHash = setting.GetLoadBalancer().GetConsistentHash()
			}
			break
		}
	}

	return consistentHash, mergedDR
}

func hashForVirtualService(push *model.PushContext,
	node *model.Proxy,
	virtualService config.Config,
) (DestinationHashMap, []*model.ConsolidatedDestRule) {
	hashByDestination := DestinationHashMap{}
	destinationRules := make([]*model.ConsolidatedDestRule, 0)
	for _, httpRoute := range virtualService.Spec.(*networking.VirtualService).Http {
		for _, destination := range httpRoute.Route {
			hash, dr := HashForHTTPDestination(push, node, destination)
			if hash != nil {
				hashByDestination[destination] = hash
				destinationRules = append(destinationRules, dr)
			}
		}
	}
	return hashByDestination, destinationRules
}

func GetConsistentHashForVirtualService(push *model.PushContext, node *model.Proxy, virtualService config.Config) DestinationHashMap {
	hashByDestination, _ := hashForVirtualService(push, node, virtualService)
	return hashByDestination
}

// HashForHTTPDestination return the ConsistentHashLB and the DestinationRule associated with HTTP route destination.
func HashForHTTPDestination(push *model.PushContext, node *model.Proxy,
	dst *networking.HTTPRouteDestination,
) (*networking.LoadBalancerSettings_ConsistentHashLB, *model.ConsolidatedDestRule) {
	if push == nil {
		return nil, nil
	}

	destination := dst.GetDestination()
	mergedDR := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, host.Name(destination.Host))
	destinationRule := mergedDR.GetRule()
	if destinationRule == nil {
		return nil, nil
	}

	rule := destinationRule.Spec.(*networking.DestinationRule)

	consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash()
	portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings()
	plsHash := portLevelSettingsConsistentHash(destination, portLevelSettings)

	var subsetHash, subsetPLSHash *networking.LoadBalancerSettings_ConsistentHashLB
	for _, subset := range rule.GetSubsets() {
		if subset.GetName() == destination.GetSubset() {
			subsetPortLevelSettings := subset.GetTrafficPolicy().GetPortLevelSettings()
			subsetHash = subset.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash()
			subsetPLSHash = portLevelSettingsConsistentHash(destination, subsetPortLevelSettings)
			break
		}
	}

	switch {
	case subsetPLSHash != nil:
		consistentHash = subsetPLSHash
	case subsetHash != nil:
		consistentHash = subsetHash
	case plsHash != nil:
		consistentHash = plsHash
	}
	return consistentHash, mergedDR
}

// SortVHostRoutes moves the catch all routes alone to the end, while retaining
// the relative order of other routes in the slice.
func SortVHostRoutes(routes []*route.Route) []*route.Route {
	allroutes := make([]*route.Route, 0, len(routes))
	catchAllRoutes := make([]*route.Route, 0)
	for _, r := range routes {
		if IsCatchAllRoute(r) {
			catchAllRoutes = append(catchAllRoutes, r)
		} else {
			allroutes = append(allroutes, r)
		}
	}
	return append(allroutes, catchAllRoutes...)
}

// IsCatchAllRoute returns true if an Envoy route is a catchall route otherwise false.
func IsCatchAllRoute(r *route.Route) bool {
	catchall := false
	// A Match is catch all if and only if it has no header/query param match
	// and URI has a prefix `/` or regex `.*`.
	switch ir := r.Match.PathSpecifier.(type) {
	case *route.RouteMatch_Prefix:
		catchall = ir.Prefix == "/"
	case *route.RouteMatch_PathSeparatedPrefix:
		catchall = ir.PathSeparatedPrefix == "/"
	case *route.RouteMatch_SafeRegex:
		catchall = ir.SafeRegex.GetRegex() == ".*"
	}

	return catchall && len(r.Match.Headers) == 0 && len(r.Match.QueryParameters) == 0 && len(r.Match.DynamicMetadata) == 0
}

func cutPrefix(s, prefix string) (after string, found bool) {
	if !strings.HasPrefix(s, prefix) {
		return s, false
	}
	return s[len(prefix):], true
}

// CheckAndGetInferencePoolConfigs extracts inference pool configurations from a VirtualService's Extra field.
// The expected structure in Extra is map[string]model.InferencePoolRouteRuleConfig.
func CheckAndGetInferencePoolConfigs(virtualService config.Config) map[string]kube.InferencePoolRouteRuleConfig {
	if virtualService.Extra != nil {
		if infPoolConfigs, ok := virtualService.Extra[constants.ConfigExtraPerRouteRuleInferencePoolConfigs].(map[string]kube.InferencePoolRouteRuleConfig); ok {
			return infPoolConfigs
		}
	}
	return nil
}
