// 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"
	xdshttpfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3"
	matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
	xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
	"github.com/golang/protobuf/ptypes/duration"
	anypb "google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/durationpb"
	wrappers "google.golang.org/protobuf/types/known/wrapperspb"

	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/networking/core/v1alpha3/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/constant"
	"istio.io/istio/pilot/pkg/util/protoconv"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/proto"
	"istio.io/istio/pkg/util/grpc"
	"istio.io/pkg/log"
)

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

// prefixMatchRegex optionally matches "/..." at the end of a path.
// regex taken from https://github.com/projectcontour/contour/blob/2b3376449bedfea7b8cea5fbade99fb64009c0f6/internal/envoy/v3/route.go#L59
const prefixMatchRegex = `((\/).*)?`

var notimeout = durationpb.New(0)

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,
) []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.Mesh)
		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 {
		for _, port := range svc.Ports {
			if port.Protocol.IsHTTP() || util.IsProtocolSniffingEnabledForPort(port) {
				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.Mesh))
			}
		}
	}

	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,
) ([]string, []*model.Service) {
	rule := virtualService.Spec.(*networking.VirtualService)
	hosts := make([]string, 0)
	servicesInVirtualService := 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(hostname)
		if !vshost.IsWildCarded() {
			if svc, exists := serviceRegistry[vshost]; exists {
				servicesInVirtualService = append(servicesInVirtualService, svc)
			} else {
				hosts = append(hosts, hostname)
			}
		} else {
			// Add it to the wildcard hosts so that they can be processed later.
			wchosts = append(wchosts, vshost)
		}
	}

	// 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) {
			hosts = append(hosts, string(hostname))
			continue
		}
		// Say host is *.global
		foundSvcMatch := false
		// Say we have Services *.foo.global, *.bar.global
		for svcHost, svc := range serviceRegistry {
			// *.foo.global matches *.global
			if svcHost.Matches(hostname) {
				servicesInVirtualService = append(servicesInVirtualService, svc)
				foundSvcMatch = true
			}
		}
		if !foundSvcMatch {
			hosts = append(hosts, string(hostname))
		}
	}

	return hosts, servicesInVirtualService
}

// 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,
	mesh *meshconfig.MeshConfig,
) []VirtualHostWrapper {
	meshGateway := map[string]bool{constants.IstioMeshGateway: true}
	routes, err := BuildHTTPRoutesForVirtualService(node, virtualService, serviceRegistry, hashByDestination,
		listenPort, meshGateway, false /* isH3DiscoveryNeeded */, mesh)
	if err != nil || len(routes) == 0 {
		return nil
	}

	hosts, servicesInVirtualService := separateVSHostsAndServices(virtualService, serviceRegistry)

	// 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 servicesInVirtualService {
		for _, port := range svc.Ports {
			if port.Protocol.IsHTTP() || util.IsProtocolSniffingEnabledForPort(port) {
				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,
	mesh *meshconfig.MeshConfig,
) VirtualHostWrapper {
	cluster := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", svc.Hostname, port.Port)
	traceOperation := telemetry.TraceOperation(string(svc.Hostname), port.Port)
	httpRoute := BuildDefaultHTTPOutboundRoute(cluster, traceOperation, 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, or error if no cluster
// can be found. Called by translateRule to determine if
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"
	}
	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, host.Name(destination.Host), port)
}

// 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,
	serviceRegistry map[host.Name]*model.Service,
	hashByDestination DestinationHashMap,
	listenPort int,
	gatewayNames map[string]bool,
	isHTTP3AltSvcHeaderNeeded bool,
	mesh *meshconfig.MeshConfig,
) ([]*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, serviceRegistry,
				hashByDestination, gatewayNames, isHTTP3AltSvcHeaderNeeded, mesh); r != nil {
				out = append(out, r)
			}
			catchall = true
		} else {
			for _, match := range http.Match {
				if r := translateRoute(node, http, match, listenPort, virtualService, serviceRegistry,
					hashByDestination, gatewayNames, isHTTP3AltSvcHeaderNeeded, mesh); 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 top 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 map[string]bool, 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[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,
	serviceRegistry map[host.Name]*model.Service,
	hashByDestination DestinationHashMap,
	gatewayNames map[string]bool,
	isHTTP3AltSvcHeaderNeeded bool,
	mesh *meshconfig.MeshConfig,
) *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(node, 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
	}

	if in.Redirect != nil {
		applyRedirect(out, in.Redirect, listenPort, model.UseGatewaySemantics(virtualService))
	} else if in.DirectResponse != nil {
		applyDirectResponse(out, in.DirectResponse)
	} else {
		applyHTTPRouteDestination(out, node, virtualService, in, mesh, authority, serviceRegistry, listenPort, hashByDestination)
	}

	out.Decorator = &route.Decorator{
		Operation: getRouteOperation(out, virtualService.Name, listenPort),
	}
	if in.Fault != nil {
		out.TypedPerFilterConfig = make(map[string]*anypb.Any)
		out.TypedPerFilterConfig[wellknown.Fault] = protoconv.MessageToAny(translateFault(in.Fault))
	}

	if 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,
	mesh *meshconfig.MeshConfig,
	authority string,
	serviceRegistry map[host.Name]*model.Service,
	listenerPort int,
	hashByDestination DestinationHashMap,
) {
	policy := in.Retries
	if policy == nil {
		// No VS policy set, use mesh defaults
		policy = mesh.GetDefaultHttpRetryPolicy()
	}
	action := &route.RouteAction{
		Cors:        translateCORSPolicy(in.CorsPolicy),
		RetryPolicy: retry.ConvertPolicy(policy),
	}

	setTimeout(action, in.Timeout, node)

	if model.UseGatewaySemantics(vs) && util.IsIstioVersionGE115(node.IstioVersion) {
		// 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 {
		action.ClusterSpecifier = &route.RouteAction_Cluster{
			Cluster: in.Name,
		}
		uri := in.Rewrite.GetUri()
		if fullURI, isFullPathRewrite := cutPrefix(uri, "%FULLREPLACE()%"); isFullPathRewrite && model.UseGatewaySemantics(vs) {
			action.RegexRewrite = &matcher.RegexMatchAndSubstitute{
				Pattern: &matcher.RegexMatcher{
					Regex: "/.+",
				},
				Substitution: fullURI,
			}
		} 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 {
			action.RequestMirrorPolicies = []*route.RouteAction_RequestMirrorPolicy{{
				Cluster:         GetDestinationCluster(in.Mirror, serviceRegistry[host.Name(in.Mirror.Host)], listenerPort),
				RuntimeFraction: mp,
				TraceSampled:    &wrappers.BoolValue{Value: false},
			}}
		}
	}

	var totalWeight uint32
	// TODO: eliminate this logic and use the total_weight option in envoy route
	weighted := make([]*route.WeightedCluster_ClusterWeight, 0)
	for _, dst := range in.Route {
		weight := &wrappers.UInt32Value{Value: uint32(dst.Weight)}
		if dst.Weight == 0 {
			// Ignore 0 weighted clusters if there are other clusters in the route.
			// But if this is the only cluster in the route, then add it as a cluster with weight 100
			if len(in.Route) == 1 {
				weight.Value = uint32(100)
			} else {
				continue
			}
		}
		hostname := host.Name(dst.GetDestination().GetHost())
		n := GetDestinationCluster(dst.Destination, serviceRegistry[hostname], listenerPort)
		clusterWeight := &route.WeightedCluster_ClusterWeight{
			Name:   n,
			Weight: weight,
		}
		totalWeight += weight.GetValue()
		if dst.Headers != nil {
			operations := translateHeadersOperations(dst.Headers)
			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,
				}
			}
		}

		weighted = append(weighted, clusterWeight)
		hash := hashByDestination[dst]
		hashPolicy := consistentHashToHashPolicy(hash)
		if hashPolicy != nil {
			action.HashPolicy = append(action.HashPolicy, hashPolicy)
		}
	}

	// rewrite to a single cluster if there is only weighted cluster
	if len(weighted) == 1 {
		action.ClusterSpecifier = &route.RouteAction_Cluster{Cluster: weighted[0].Name}
		out.RequestHeadersToAdd = append(out.RequestHeadersToAdd, weighted[0].RequestHeadersToAdd...)
		out.RequestHeadersToRemove = append(out.RequestHeadersToRemove, weighted[0].RequestHeadersToRemove...)
		out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, weighted[0].ResponseHeadersToAdd...)
		out.ResponseHeadersToRemove = append(out.ResponseHeadersToRemove, weighted[0].ResponseHeadersToRemove...)
		if weighted[0].HostRewriteSpecifier != nil && 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: weighted[0].GetHostRewriteLiteral(),
			}
		}
	} else {
		action.ClusterSpecifier = &route.RouteAction_WeightedClusters{
			WeightedClusters: &route.WeightedCluster{
				Clusters:    weighted,
				TotalWeight: wrappers.UInt32(totalWeight),
			},
		}
	}
}

func applyRedirect(out *route.Route, redirect *networking.HTTPRedirect, port int, 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
		}
	}

	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)
		action = nil
	}

	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{
		Append: proto.BoolTrue,
		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),
		}
	}
}

// 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
		}
		headerValueOptionList = append(headerValueOptionList, &core.HeaderValueOption{
			Header: &core.HeaderValue{
				Key:   key,
				Value: value,
			},
			Append: &wrappers.BoolValue{Value: appendFlag},
		})
	}
	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(node *model.Proxy, 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
			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, "/")
				if util.IsIstioVersionGE114(node.IstioVersion) {
					out.PathSpecifier = &route.RouteMatch_PathSeparatedPrefix{PathSeparatedPrefix: path}
				} else {
					// For older versions, we have to use the regex hack.
					// From the spec: /foo/bar matches /foo/bar/baz, but does not match /foo/barbaz
					// and if the prefix is /foo/bar/ we must match /foo/bar and /foo/bar/baz. We cannot simply strip the
					// trailing "/" and do a prefix match since we'll match unwanted continuations and we cannot add
					// a "/" if not present since we won't match the prefix without trailing "/". Must be smarter and
					// use regex.
					out.PathSpecifier = &route.RouteMatch_SafeRegex{
						SafeRegex: &matcher.RegexMatcher{
							Regex: regexp.QuoteMeta(path) + prefixMatchRegex,
						},
					}
				}
			} 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 = &wrappers.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,
	}

	switch m := in.MatchType.(type) {
	case *networking.StringMatch_Exact:
		out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{
			StringMatch: &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Exact{Exact: m.Exact}},
		}
	case *networking.StringMatch_Regex:
		out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{
			StringMatch: &matcher.StringMatcher{
				MatchPattern: &matcher.StringMatcher_SafeRegex{
					SafeRegex: &matcher.RegexMatcher{
						Regex: m.Regex,
					},
				},
			},
		}
	}

	return out
}

// isCatchAllHeaderMatch determines if the given header is matched with all strings or not.
// Currently, if the regex has "*" value, it returns true
func isCatchAllHeaderMatch(in *networking.StringMatch) bool {
	if in == nil {
		return true
	}

	catchall := false

	switch m := in.MatchType.(type) {
	case *networking.StringMatch_Regex:
		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 `.` as a separator for claim names.
// Examples:
// - `@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 {
	if !strings.HasPrefix(strings.ToLower(name), constant.HeaderJWTClaim) {
		return nil
	}
	claims := strings.Split(name[len(constant.HeaderJWTClaim):], ".")
	return authz.MetadataMatcherForJWTClaims(claims, util.ConvertToEnvoyMatch(in))
}

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

	if isCatchAllHeaderMatch(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
}

// translateCORSPolicy translates CORS policy
func translateCORSPolicy(in *networking.CorsPolicy) *route.CorsPolicy {
	if in == nil {
		return nil
	}

	// CORS filter is enabled by default
	out := route.CorsPolicy{}
	// nolint: staticcheck
	if in.AllowOrigins != nil {
		out.AllowOriginStringMatch = util.ConvertToEnvoyMatches(in.AllowOrigins)
	} else if in.AllowOrigin != nil {
		out.AllowOriginStringMatch = util.StringToExactMatch(in.AllowOrigin)
	}

	out.EnabledSpecifier = &route.CorsPolicy_FilterEnabled{
		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 serivce 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(clusterName string, operation string) *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,
	}
	return out
}

func buildDefaultHTTPRoute(clusterName string, operation string) *route.Route {
	routeAction := &route.RouteAction{
		ClusterSpecifier: &route.RouteAction_Cluster{Cluster: clusterName},
	}
	val := &route.Route{
		Match: translateRouteMatch(nil, 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 *duration.Duration, node *model.Proxy) {
	// Configure timeouts specified by Virtual Service if they are provided, otherwise set it to defaults.
	action.Timeout = features.DefaultRequestTimeout
	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())
	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 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()
		}
		return &route.RouteAction_HashPolicy{
			PolicySpecifier: &route.RouteAction_HashPolicy_Cookie_{
				Cookie: &route.RouteAction_HashPolicy_Cookie{
					Name: cookie.GetName(),
					Ttl:  ttl,
					Path: cookie.GetPath(),
				},
			},
		}
	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
}

// isCatchAll returns true if HTTPMatchRequest is a catchall match otherwise
// false. Note - this may not be exactly "catch all" as we don't know the full
// class of possible inputs As such, this is used only for optimization.
func isCatchAllMatch(m *networking.HTTPMatchRequest) bool {
	catchall := false
	if m.Uri != nil {
		switch m := m.Uri.MatchType.(type) {
		case *networking.StringMatch_Prefix:
			catchall = m.Prefix == "/"
		case *networking.StringMatch_Regex:
			catchall = m.Regex == "*"
		}
	}
	// A Match is catch all if and only if it has no match set
	// and URI has a prefix / or regex *.
	return catchall &&
		len(m.Headers) == 0 &&
		len(m.QueryParams) == 0 &&
		len(m.SourceLabels) == 0 &&
		len(m.WithoutHeaders) == 0 &&
		len(m.Gateways) == 0 &&
		m.Method == nil &&
		m.Scheme == nil &&
		m.Port == 0 &&
		m.Authority == nil &&
		m.SourceNamespace == ""
}

// 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
	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() == "*"
	}
	// A Match is catch all if and only if it has no header/query param match
	// and URI has a prefix / or regex *.
	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
}
