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

import (
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	tracingcfg "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3"
	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	otelsamplers "github.com/envoyproxy/go-control-plane/envoy/extensions/tracers/opentelemetry/samplers/v3"
	envoy_type_metadata_v3 "github.com/envoyproxy/go-control-plane/envoy/type/metadata/v3"
	tracing "github.com/envoyproxy/go-control-plane/envoy/type/tracing/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"

	meshconfig "istio.io/api/mesh/v1alpha1"
	telemetrypb "istio.io/api/telemetry/v1alpha1"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/networking"
	authz_model "istio.io/istio/pilot/pkg/security/authz/model"
	"istio.io/istio/pilot/pkg/util/protoconv"
	xdsfilters "istio.io/istio/pilot/pkg/xds/filters"
	"istio.io/istio/pilot/pkg/xds/requestidextension"
	"istio.io/istio/pkg/env"
	"istio.io/istio/pkg/log"
	"istio.io/istio/pkg/wellknown"
)

const (
	envoyDatadog       = "envoy.tracers.datadog"
	envoyOpenTelemetry = "envoy.tracers.opentelemetry"
	envoySkywalking    = "envoy.tracers.skywalking"
	envoyZipkin        = "envoy.tracers.zipkin"
)

// this is used for testing. it should not be changed in regular code.
var clusterLookupFn = model.LookupCluster

type typedConfigGenFn func() (*anypb.Any, error)

func configureTracing(
	push *model.PushContext,
	proxy *model.Proxy,
	httpConnMgr *hcm.HttpConnectionManager,
	class networking.ListenerClass,
	svc *model.Service,
) *requestidextension.UUIDRequestIDExtensionContext {
	tracingCfg := push.Telemetry.Tracing(proxy, svc)
	return configureTracingFromTelemetry(tracingCfg, push, proxy, httpConnMgr, class)
}

func configureTracingFromTelemetry(
	tracing *model.TracingConfig,
	push *model.PushContext,
	proxy *model.Proxy,
	h *hcm.HttpConnectionManager,
	class networking.ListenerClass,
) *requestidextension.UUIDRequestIDExtensionContext {
	proxyCfg := proxy.Metadata.ProxyConfigOrDefault(push.Mesh.DefaultConfig)
	// If there is no telemetry config defined, fallback to legacy mesh config.
	if tracing == nil {
		meshCfg := push.Mesh
		if !meshCfg.EnableTracing {
			log.Debug("No valid tracing configuration found")
			return nil
		}
		// use the prior configuration bits of sampling and custom tags
		h.Tracing = &hcm.HttpConnectionManager_Tracing{}
		configureSampling(h.Tracing, proxyConfigSamplingValue(proxyCfg))
		configureCustomTags(nil, h.Tracing, map[string]*telemetrypb.Tracing_CustomTag{}, proxyCfg, proxy)
		if proxyCfg.GetTracing().GetMaxPathTagLength() != 0 {
			h.Tracing.MaxPathTagLength = wrapperspb.UInt32(proxyCfg.GetTracing().MaxPathTagLength)
		}
		return nil
	}
	spec := tracing.ServerSpec
	if class == networking.ListenerClassSidecarOutbound || class == networking.ListenerClassGateway {
		spec = tracing.ClientSpec
	}

	if spec.Disabled {
		return nil
	}

	var useCustomSampler bool
	if spec.Provider != nil {
		hcmTracing, hasCustomSampler, err := configureFromProviderConfig(push, proxy, spec.Provider)
		if err != nil {
			log.Warnf("Not able to configure requested tracing provider %q: %v", spec.Provider.Name, err)
			return nil
		}
		h.Tracing = hcmTracing
		useCustomSampler = hasCustomSampler
	} else {
		// TODO: should this `return nil, nil` instead ?
		log.Warnf("Not able to configure tracing provider. Provider lookup failed.")
		h.Tracing = &hcm.HttpConnectionManager_Tracing{}
		// TODO: transition to configuring providers from proxy config here?
		// something like: configureFromProxyConfig(tracingCfg, opts.proxy.Metadata.ProxyConfig.Tracing)
	}

	// provider.sampler > telemetry.RandomSamplingPercentage > defaultConfig.tracing.sampling > PILOT_TRACE_SAMPLING
	var sampling float64
	if useCustomSampler {
		// If the TracingProvider has a custom sampler (OTel Sampler)
		// the sampling percentage is set to 100% so all spans arrive at the sampler for its decision.
		sampling = 100
	} else if spec.RandomSamplingPercentage != nil {
		sampling = *spec.RandomSamplingPercentage
	} else {
		// gracefully fallback to MeshConfig configuration. It will act as an implicit
		// parent configuration during transition period.
		sampling = proxyConfigSamplingValue(proxyCfg)
	}

	configureSampling(h.Tracing, sampling)
	configureCustomTags(&spec, h.Tracing, spec.CustomTags, proxyCfg, proxy)

	// if there is configured max tag length somewhere, fallback to it.
	if h.GetTracing().GetMaxPathTagLength() == nil && proxyCfg.GetTracing().GetMaxPathTagLength() != 0 {
		h.Tracing.MaxPathTagLength = wrapperspb.UInt32(proxyCfg.GetTracing().MaxPathTagLength)
	}

	if spec.DisableContextPropagation {
		if proxy.IstioVersion != nil && proxy.VersionGreaterOrEqual(&model.IstioVersion{Major: 1, Minor: 30}) {
			h.Tracing.NoContextPropagation = true
		} else {
			log.Debugf("Proxy %s (version %v) does not support NoContextPropagation: requires Istio 1.30+",
				proxy.ID, proxy.IstioVersion)
		}
	}

	reqIDExtension := &requestidextension.UUIDRequestIDExtensionContext{}
	reqIDExtension.UseRequestIDForTraceSampling = spec.UseRequestIDForTraceSampling
	return reqIDExtension
}

// configureFromProviderConfigHandled contains the number of providers we handle below.
// This is to ensure this stays in sync as new handlers are added
// STOP. DO NOT UPDATE THIS WITHOUT UPDATING configureFromProviderConfig.
const configureFromProviderConfigHandled = 15

func configureFromProviderConfig(pushCtx *model.PushContext, proxy *model.Proxy,
	providerCfg *meshconfig.MeshConfig_ExtensionProvider,
) (*hcm.HttpConnectionManager_Tracing, bool, error) {
	startChildSpan := false
	if proxy.Type == model.Router {
		startChildSpan = features.SpawnUpstreamSpanForGateway
	}
	useCustomSampler := false
	var serviceCluster string
	var maxTagLength uint32
	var providerConfig typedConfigGenFn
	var providerName string
	// Span service name must identify the producer of the span (the proxy), not the destination.
	if proxy.XdsNode != nil {
		serviceCluster = proxy.XdsNode.Cluster
	}
	switch provider := providerCfg.Provider.(type) {
	case *meshconfig.MeshConfig_ExtensionProvider_Zipkin:
		maxTagLength = provider.Zipkin.GetMaxTagLength()
		providerName = envoyZipkin
		providerConfig = func() (*anypb.Any, error) {
			hostname, cluster, err := clusterLookupFn(pushCtx, provider.Zipkin.GetService(), int(provider.Zipkin.GetPort()))
			if err != nil {
				model.IncLookupClusterFailures("zipkin")
				return nil, fmt.Errorf("could not find cluster for tracing provider %q: %v", provider, err)
			}
			traceContextOption := convertTraceContextOption(provider.Zipkin.GetTraceContextOption())
			return zipkinConfig(hostname, cluster, provider.Zipkin.GetPath(), !provider.Zipkin.GetEnable_64BitTraceId(),
				traceContextOption, provider.Zipkin.GetTimeout(), provider.Zipkin.GetHeaders(), proxy)
		}
	case *meshconfig.MeshConfig_ExtensionProvider_Datadog:
		maxTagLength = provider.Datadog.GetMaxTagLength()
		providerName = envoyDatadog
		providerConfig = func() (*anypb.Any, error) {
			hostname, cluster, err := clusterLookupFn(pushCtx, provider.Datadog.GetService(), int(provider.Datadog.GetPort()))
			if err != nil {
				model.IncLookupClusterFailures("datadog")
				return nil, fmt.Errorf("could not find cluster for tracing provider %q: %v", provider, err)
			}
			return datadogConfig(serviceCluster, hostname, cluster)
		}
	case *meshconfig.MeshConfig_ExtensionProvider_Lightstep:
		log.Warnf("Lightstep provider is deprecated, please use OpenTelemetry instead")
	case *meshconfig.MeshConfig_ExtensionProvider_Skywalking:
		maxTagLength = 0
		providerName = envoySkywalking
		providerConfig = func() (*anypb.Any, error) {
			hostname, clusterName, err := clusterLookupFn(pushCtx, provider.Skywalking.GetService(), int(provider.Skywalking.GetPort()))
			if err != nil {
				model.IncLookupClusterFailures("skywalking")
				return nil, fmt.Errorf("could not find cluster for tracing provider %q: %v", provider, err)
			}
			return skywalkingConfig(clusterName, hostname)
		}

		startChildSpan = true
	case *meshconfig.MeshConfig_ExtensionProvider_Opentelemetry:
		maxTagLength = provider.Opentelemetry.GetMaxTagLength()
		providerName = envoyOpenTelemetry
		providerConfig = func() (*anypb.Any, error) {
			tracingCfg, hasCustomSampler, err := otelConfig(serviceCluster, provider.Opentelemetry, pushCtx, proxy)
			useCustomSampler = hasCustomSampler
			return tracingCfg, err
		}
		// Providers without any tracing support
		// Explicitly list to be clear what does and does not support tracing
	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp,
		*meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc,
		*meshconfig.MeshConfig_ExtensionProvider_EnvoyHttpAls,
		*meshconfig.MeshConfig_ExtensionProvider_EnvoyTcpAls,
		*meshconfig.MeshConfig_ExtensionProvider_EnvoyOtelAls,
		*meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLog,
		*meshconfig.MeshConfig_ExtensionProvider_Sds,
		*meshconfig.MeshConfig_ExtensionProvider_Prometheus:
		return nil, false, fmt.Errorf("provider %T does not support tracing", provider)
		// Should never happen, but just in case we forget to add one
	default:
		return nil, false, fmt.Errorf("provider %T does not support tracing", provider)
	}
	hcmTracing, err := buildHCMTracing(providerName, startChildSpan, maxTagLength, providerConfig)
	return hcmTracing, useCustomSampler, err
}

// convertTraceContextOption converts meshconfig ZipkinTraceContextOption to Envoy ZipkinConfig_TraceContextOption
func convertTraceContextOption(
	option meshconfig.MeshConfig_ExtensionProvider_ZipkinTracingProvider_TraceContextOption,
) tracingcfg.ZipkinConfig_TraceContextOption {
	switch option {
	case meshconfig.MeshConfig_ExtensionProvider_ZipkinTracingProvider_USE_B3_WITH_W3C_PROPAGATION:
		return tracingcfg.ZipkinConfig_USE_B3_WITH_W3C_PROPAGATION
	default:
		// Default to USE_B3 for backward compatibility
		return tracingcfg.ZipkinConfig_USE_B3
	}
}

func zipkinConfig(
	hostname, cluster, endpoint string, enable128BitTraceID bool, traceContextOption tracingcfg.ZipkinConfig_TraceContextOption,
	timeout *durationpb.Duration, headers []*meshconfig.MeshConfig_ExtensionProvider_HttpHeader, proxy *model.Proxy,
) (*anypb.Any, error) {
	if endpoint == "" {
		endpoint = "/api/v2/spans" // envoy deprecated v1 support
	}
	zc := &tracingcfg.ZipkinConfig{
		CollectorEndpointVersion: tracingcfg.ZipkinConfig_HTTP_JSON, // use v2 JSON for now
		TraceId_128Bit:           enable128BitTraceID,               // istio default enable 128 bit trace id
		SharedSpanContext:        wrapperspb.Bool(false),
	}

	// Determine if we should use HttpService (modern approach) or legacy fields
	// For newer proxies (Istio 1.29+), always use HttpService as it's the modern approach
	// that supports timeout and custom headers. For older proxies, use legacy fields for
	// backward compatibility.
	proxyVersionGreaterOrEqual129 := proxy.IstioVersion != nil && proxy.VersionGreaterOrEqual(&model.IstioVersion{Major: 1, Minor: 29})

	if proxyVersionGreaterOrEqual129 {
		// Modern configuration using HttpService
		// This is required for timeout and custom headers support

		// Set default timeout if not provided, as Envoy requires a timeout to be set.
		if timeout == nil {
			timeout = durationpb.New(5 * time.Second)
		}

		httpService := &core.HttpService{
			HttpUri: &core.HttpUri{
				Uri: fmt.Sprintf("http://%s%s", hostname, endpoint),
				HttpUpstreamType: &core.HttpUri_Cluster{
					Cluster: cluster,
				},
				Timeout: timeout,
			},
		}
		// Add custom headers if provided
		if len(headers) > 0 {
			httpService.RequestHeadersToAdd = buildHTTPHeaders(headers)
		}
		zc.CollectorService = httpService
	} else {
		// Legacy configuration - maintains backward compatibility for older proxies
		zc.CollectorCluster = cluster
		zc.CollectorEndpoint = endpoint
		zc.CollectorHostname = hostname // http host header

		useHTTPService := timeout != nil || len(headers) > 0
		// Log when timeout/headers configuration is ignored due to older proxy version
		if useHTTPService {
			log.Warnf("Proxy %s (version %v) does not support Zipkin timeout/headers configuration: requires Istio 1.29+. "+
				"Configuration will be ignored.", proxy.ID, proxy.IstioVersion)
		}
	}

	// Only set TraceContextOption for proxies that support it to avoid NACK from older proxies
	// TraceContextOption support requires a recent Envoy version - using conservative version gating
	if proxy.IstioVersion != nil && proxy.VersionGreaterOrEqual(&model.IstioVersion{Major: 1, Minor: 28}) {
		zc.TraceContextOption = traceContextOption
	} else {
		// Log when TraceContextOption configuration is ignored due to version gating
		log.Debugf("Proxy %s (version %v) does not support TraceContextOption: requires Istio 1.28+",
			proxy.ID, proxy.IstioVersion)
	}

	return protoconv.MessageToAnyWithError(zc)
}

func datadogConfig(serviceName, hostname, cluster string) (*anypb.Any, error) {
	dc := &tracingcfg.DatadogConfig{
		CollectorCluster:  cluster,
		ServiceName:       serviceName,
		CollectorHostname: hostname,
	}
	return protoconv.MessageToAnyWithError(dc)
}

func otelConfig(serviceName string, otelProvider *meshconfig.MeshConfig_ExtensionProvider_OpenTelemetryTracingProvider,
	pushCtx *model.PushContext, proxy *model.Proxy,
) (*anypb.Any, bool, error) {
	hostname, cluster, err := clusterLookupFn(pushCtx, otelProvider.GetService(), int(otelProvider.GetPort()))
	if err != nil {
		model.IncLookupClusterFailures("opentelemetry")
		return nil, false, fmt.Errorf("could not find cluster for tracing provider %q: %v", otelProvider, err)
	}

	// When OTel semantic conventions are enabled, compute service.name
	// following the OTel K8s service attributes specification.
	if otelProvider.GetServiceAttributeEnrichment() == meshconfig.MeshConfig_ExtensionProvider_OTEL_SEMANTIC_CONVENTIONS {
		serviceName = otelServiceName(proxy)
	}

	hasCustomSampler := false
	oc := &tracingcfg.OpenTelemetryConfig{
		ServiceName: serviceName,
	}

	if otelProvider.GetHttp() != nil {
		// export via HTTP
		httpService := otelProvider.GetHttp()
		te, err := url.JoinPath(hostname, httpService.GetPath())
		if err != nil {
			return nil, false, fmt.Errorf("could not parse otlp/http traces endpoint: %v", err)
		}
		oc.HttpService = &core.HttpService{
			HttpUri: &core.HttpUri{
				Uri: te,
				HttpUpstreamType: &core.HttpUri_Cluster{
					Cluster: cluster,
				},
				Timeout: httpService.GetTimeout(),
			},
			RequestHeadersToAdd: buildHTTPHeaders(httpService.GetHeaders()),
		}

	} else {
		// export via gRPC
		oc.GrpcService = &core.GrpcService{
			TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
				EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
					ClusterName: cluster,
					Authority:   hostname,
				},
			},
			Timeout:         otelProvider.GetGrpc().GetTimeout(),
			InitialMetadata: buildInitialMetadata(otelProvider.GetGrpc().GetInitialMetadata()),
		}
	}

	// Add configured resource detectors
	{
		res := []*core.TypedExtensionConfig{}
		if rd := otelProvider.ResourceDetectors; rd != nil {
			if rd.Environment != nil {
				res = append(res, xdsfilters.EnvironmentResourceDetector)
			}
			if rd.Dynatrace != nil {
				res = append(res, xdsfilters.DynatraceResourceDetector)
			}
		}
		// When OTel semantic conventions are enabled, auto-enable the Environment
		// resource detector so that OTEL_RESOURCE_ATTRIBUTES env var (if set on the
		// proxy) can provide or override resource attributes such as service.namespace,
		// service.version, and service.instance.id.
		if otelProvider.GetServiceAttributeEnrichment() == meshconfig.MeshConfig_ExtensionProvider_OTEL_SEMANTIC_CONVENTIONS {
			hasEnvDetector := false
			for _, r := range res {
				if r.Name == xdsfilters.EnvironmentResourceDetector.Name {
					hasEnvDetector = true
					break
				}
			}
			if !hasEnvDetector {
				res = append(res, xdsfilters.EnvironmentResourceDetector)
			}
		}
		if len(res) > 0 {
			oc.ResourceDetectors = res
		}
	}

	// Add configured Sampler
	if otelProvider.Sampling != nil {
		switch sampler := otelProvider.Sampling.(type) {
		case *meshconfig.MeshConfig_ExtensionProvider_OpenTelemetryTracingProvider_DynatraceSampler_:
			dts, err := configureDynatraceSampler(hostname, cluster, otelProvider, sampler, pushCtx)
			if err != nil {
				return nil, false, err
			}
			oc.Sampler = dts
		}

		// If any sampler is configured for the OpenTelemetryTracingProvider
		hasCustomSampler = true
	}

	pb, err := anypb.New(oc)
	return pb, hasCustomSampler, err
}

func skywalkingConfig(clusterName, hostname string) (*anypb.Any, error) {
	s := &tracingcfg.SkyWalkingConfig{
		GrpcService: &core.GrpcService{
			TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
				EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
					ClusterName: clusterName,
					Authority:   hostname,
				},
			},
		},
	}

	return protoconv.MessageToAnyWithError(s)
}

func configureDynatraceSampler(hostname, cluster string,
	otelProvider *meshconfig.MeshConfig_ExtensionProvider_OpenTelemetryTracingProvider,
	sampler *meshconfig.MeshConfig_ExtensionProvider_OpenTelemetryTracingProvider_DynatraceSampler_,
	pushCtx *model.PushContext,
) (*core.TypedExtensionConfig, error) {
	dsc := &otelsamplers.DynatraceSamplerConfig{
		Tenant:             sampler.DynatraceSampler.GetTenant(),
		ClusterId:          sampler.DynatraceSampler.GetClusterId(),
		RootSpansPerMinute: sampler.DynatraceSampler.GetRootSpansPerMinute(),
	}

	if sampler.DynatraceSampler.HttpService == nil {
		// The Dynatrace sampler can re-use the same Dynatrace service/cluster/headers
		// as configured for the HTTP Exporter. In this case users
		// can achieve a much smaller/simpler config in Istio.

		// Re-use the Dynatrace API Host from the OTLP HTTP exporter
		otlpHTTPService := otelProvider.GetHttp()
		if otlpHTTPService == nil {
			return nil, fmt.Errorf("dynatrace sampler could not get http settings. considering setting the http_service field on the dynatrace sampler")
		}

		uri, err := url.JoinPath(hostname, "api/v2/samplingConfiguration")
		if err != nil {
			return nil, fmt.Errorf("could not parse dynatrace adaptative sampling endpoint %v", err)
		}

		dsc.HttpService = &core.HttpService{
			HttpUri: &core.HttpUri{
				Uri: uri,
				HttpUpstreamType: &core.HttpUri_Cluster{
					Cluster: cluster,
				},
				Timeout: otlpHTTPService.GetTimeout(),
			},
			// Re-use the headers from the OTLP HTTP Exporter
			RequestHeadersToAdd: buildHTTPHeaders(otlpHTTPService.GetHeaders()),
		}
	} else {
		// Dynatrace customers may want to export to a OTel collector
		// but still have the benefits of the Dynatrace Sampler.
		// In this case, the sampler has its own HTTP configuration
		dtapi := sampler.DynatraceSampler.HttpService

		// use Dynatrace API to configure the sampler
		dtHost, dtCluster, err := clusterLookupFn(pushCtx, dtapi.GetService(), int(dtapi.GetPort()))
		if err != nil {
			model.IncLookupClusterFailures("dynatrace")
			return nil, fmt.Errorf("could not find cluster for dynatrace sampler %v", err)
		}

		uri, err := url.JoinPath(dtHost, dtapi.GetHttp().Path)
		if err != nil {
			return nil, fmt.Errorf("could not parse dynatrace adaptative sampling endpoint %v", err)
		}

		dsc.HttpService = &core.HttpService{
			HttpUri: &core.HttpUri{
				Uri: uri,
				HttpUpstreamType: &core.HttpUri_Cluster{
					Cluster: dtCluster,
				},
				Timeout: dtapi.Http.GetTimeout(),
			},
			RequestHeadersToAdd: buildHTTPHeaders(dtapi.Http.GetHeaders()),
		}
	}

	return &core.TypedExtensionConfig{
		Name:        "envoy.tracers.opentelemetry.samplers.dynatrace",
		TypedConfig: protoconv.MessageToAny(dsc),
	}, nil
}

func buildHCMTracing(provider string, startChildSpan bool, maxTagLen uint32, anyFn typedConfigGenFn) (*hcm.HttpConnectionManager_Tracing, error) {
	config := &hcm.HttpConnectionManager_Tracing{}
	if startChildSpan {
		config.SpawnUpstreamSpan = wrapperspb.Bool(startChildSpan)
	}
	cfg, err := anyFn()
	if err != nil {
		return config, fmt.Errorf("could not configure tracing provider %q: %v", provider, err)
	}

	config.Provider = &tracingcfg.Tracing_Http{
		Name:       provider,
		ConfigType: &tracingcfg.Tracing_Http_TypedConfig{TypedConfig: cfg},
	}

	if maxTagLen != 0 {
		config.MaxPathTagLength = &wrapperspb.UInt32Value{Value: maxTagLen}
	}
	return config, nil
}

func dryRunPolicyTraceTag(name, key string) *tracing.CustomTag {
	// The tag will not be populated when not used as there is no default value set for the tag.
	// See https://www.envoyproxy.io/docs/envoy/v1.17.1/configuration/http/http_filters/rbac_filter#dynamic-metadata.
	return &tracing.CustomTag{
		Tag: name,
		Type: &tracing.CustomTag_Metadata_{
			Metadata: &tracing.CustomTag_Metadata{
				Kind: &envoy_type_metadata_v3.MetadataKind{
					Kind: &envoy_type_metadata_v3.MetadataKind_Request_{
						Request: &envoy_type_metadata_v3.MetadataKind_Request{},
					},
				},
				MetadataKey: &envoy_type_metadata_v3.MetadataKey{
					Key: wellknown.HTTPRoleBasedAccessControl,
					Path: []*envoy_type_metadata_v3.MetadataKey_PathSegment{
						{
							Segment: &envoy_type_metadata_v3.MetadataKey_PathSegment_Key{
								Key: key,
							},
						},
					},
				},
			},
		},
	}
}

var optionalPolicyTags = []*tracing.CustomTag{
	dryRunPolicyTraceTag("istio.authorization.dry_run.allow_policy.name", authz_model.RBACShadowRulesAllowStatPrefix+authz_model.RBACShadowEffectivePolicyID),
	dryRunPolicyTraceTag("istio.authorization.dry_run.allow_policy.result", authz_model.RBACShadowRulesAllowStatPrefix+authz_model.RBACShadowEngineResult),
	dryRunPolicyTraceTag("istio.authorization.dry_run.deny_policy.name", authz_model.RBACShadowRulesDenyStatPrefix+authz_model.RBACShadowEffectivePolicyID),
	dryRunPolicyTraceTag("istio.authorization.dry_run.deny_policy.result", authz_model.RBACShadowRulesDenyStatPrefix+authz_model.RBACShadowEngineResult),
}

// buildWaypointSourceTags creates custom trace tags for waypoints that capture source (client) peer info.
// Uses Envoy FILTER_STATE FIELD accessor to extract all available fields from downstream_peer_obj.
// The peer_metadata filter stores WorkloadMetadataObject under downstream_peer_obj key,
// while CelState is stored under downstream_peer key for CEL expression compatibility.
// All fields are extracted individually to support dynamic addition of new fields in WorkloadMetadataObject.
func buildWaypointSourceTags() []*tracing.CustomTag {
	buildDownstreamFilterStateTag := func(tagName, fieldName string) *tracing.CustomTag {
		return &tracing.CustomTag{
			Tag: tagName,
			Type: &tracing.CustomTag_Value{
				Value: fmt.Sprintf("%%FILTER_STATE(downstream_peer_obj:FIELD:%s)%%", fieldName),
			},
		}
	}
	buildUpstreamFilterStateTag := func(tagName, fieldName string) *tracing.CustomTag {
		return &tracing.CustomTag{
			Tag: tagName,
			Type: &tracing.CustomTag_Value{
				Value: fmt.Sprintf("%%FILTER_STATE(upstream_peer_obj:FIELD:%s)%%", fieldName),
			},
		}
	}

	// Extract all available fields from WorkloadMetadataObject
	// See: proxy/extensions/common/metadata_object.h for field definitions
	return []*tracing.CustomTag{
		buildUpstreamFilterStateTag("istio.destination_workload", "workload"),
		buildUpstreamFilterStateTag("istio.destination_namespace", "namespace"),
		buildUpstreamFilterStateTag("istio.destination_cluster_id", "cluster"),
		buildUpstreamFilterStateTag("istio.destination_canonical_service", "service"),
		buildUpstreamFilterStateTag("istio.destination_canonical_revision", "revision"),
		buildUpstreamFilterStateTag("istio.destination_app", "app"),
		buildUpstreamFilterStateTag("istio.destination_app_version", "version"),
		buildUpstreamFilterStateTag("istio.destination_workload_type", "type"),
		buildUpstreamFilterStateTag("istio.destination_instance_name", "name"),

		buildDownstreamFilterStateTag("istio.source_workload", "workload"),
		buildDownstreamFilterStateTag("istio.source_namespace", "namespace"),
		buildDownstreamFilterStateTag("istio.source_cluster_id", "cluster"),
		buildDownstreamFilterStateTag("istio.source_canonical_service", "service"),
		buildDownstreamFilterStateTag("istio.source_canonical_revision", "revision"),
		buildDownstreamFilterStateTag("istio.source_app", "app"),
		buildDownstreamFilterStateTag("istio.source_app_version", "version"),
		buildDownstreamFilterStateTag("istio.source_workload_type", "type"),
		buildDownstreamFilterStateTag("istio.source_instance_name", "name"),
	}
}

func buildServiceTags(node *model.Proxy) []*tracing.CustomTag {
	var revision, service string
	if node.Labels != nil {
		revision = node.Labels["service.istio.io/canonical-revision"]
		service = node.Labels["service.istio.io/canonical-name"]
	}
	if revision == "" {
		revision = "latest"
	}
	// TODO: This should have been properly handled with the injector.
	if service == "" {
		service = "unknown"
	}
	meshID := node.Metadata.MeshID
	if meshID == "" {
		meshID = "unknown"
	}
	namespace := node.Metadata.Namespace
	if namespace == "" {
		namespace = "default"
	}
	clusterID := string(node.Metadata.ClusterID)
	if clusterID == "" {
		clusterID = "unknown"
	}
	tags := []*tracing.CustomTag{
		{
			Tag: "istio.canonical_revision",
			Type: &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: revision,
				},
			},
		},
		{
			Tag: "istio.canonical_service",
			Type: &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: service,
				},
			},
		},
		{
			Tag: "istio.mesh_id",
			Type: &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: meshID,
				},
			},
		},
		{
			Tag: "istio.namespace",
			Type: &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: namespace,
				},
			},
		},
		{
			Tag: "istio.cluster_id",
			Type: &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: clusterID,
				},
			},
		},
	}

	return tags
}

func configureSampling(hcmTracing *hcm.HttpConnectionManager_Tracing, providerPercentage float64) {
	hcmTracing.ClientSampling = &xdstype.Percent{
		Value: 100.0,
	}
	hcmTracing.OverallSampling = &xdstype.Percent{
		Value: 100.0,
	}
	hcmTracing.RandomSampling = &xdstype.Percent{
		Value: providerPercentage,
	}
}

func proxyConfigSamplingValue(config *meshconfig.ProxyConfig) float64 {
	// PILOT_TRACE_SAMPLING
	sampling := features.TraceSampling

	// Tracing from default_config
	if config.Tracing != nil && config.Tracing.Sampling != 0.0 {
		sampling = config.Tracing.Sampling

		if sampling > 100.0 {
			sampling = 1.0
		}
	}
	return sampling
}

func configureCustomTags(spec *model.TracingSpec, hcmTracing *hcm.HttpConnectionManager_Tracing,
	providerTags map[string]*telemetrypb.Tracing_CustomTag, proxyCfg *meshconfig.ProxyConfig, node *model.Proxy,
) {
	var tags []*tracing.CustomTag
	if spec == nil {
		// Fallback to legacy mesh config.
		enableIstioTags := true
		if proxyCfg.GetTracing().GetEnableIstioTags() != nil {
			enableIstioTags = proxyCfg.GetTracing().GetEnableIstioTags().GetValue()
		}
		if enableIstioTags {
			tags = append(buildServiceTags(node), optionalPolicyTags...)
		}
	} else if spec.EnableIstioTags {
		tags = append(buildServiceTags(node), optionalPolicyTags...)
	}

	// For waypoint proxies, add source peer tags to capture client workload info.
	if node.Type == model.Waypoint {
		tags = append(tags, buildWaypointSourceTags()...)
	}

	if len(providerTags) == 0 {
		tags = append(tags, buildCustomTagsFromProxyConfig(proxyCfg.GetTracing().GetCustomTags())...)
	} else {
		tags = append(tags, buildCustomTagsFromProvider(node, providerTags)...)
	}

	// looping over customTags, a map, results in the returned value
	// being non-deterministic when multiple tags were defined; sort by the tag name
	// to rectify this
	sort.Slice(tags, func(i, j int) bool {
		return tags[i].Tag < tags[j].Tag
	})

	hcmTracing.CustomTags = tags
}

func buildCustomTagsFromProvider(node *model.Proxy, providerTags map[string]*telemetrypb.Tracing_CustomTag) []*tracing.CustomTag {
	var tags []*tracing.CustomTag

	supportFormatterTag := node.VersionGreaterOrEqual(&model.IstioVersion{Major: 1, Minor: 29, Patch: 0})
	hasFormatterTag := false

	for tagName, tagInfo := range providerTags {
		if tagInfo == nil {
			log.Warnf("while building custom tags from provider, encountered nil custom tag: %s, skipping", tagName)
			continue
		}
		t := &tracing.CustomTag{
			Tag: tagName,
		}
		switch tag := tagInfo.Type.(type) {
		case *telemetrypb.Tracing_CustomTag_Environment:
			t.Type = &tracing.CustomTag_Environment_{
				Environment: &tracing.CustomTag_Environment{
					Name:         tag.Environment.Name,
					DefaultValue: tag.Environment.DefaultValue,
				},
			}
		case *telemetrypb.Tracing_CustomTag_Header:
			t.Type = &tracing.CustomTag_RequestHeader{
				RequestHeader: &tracing.CustomTag_Header{
					Name:         tag.Header.Name,
					DefaultValue: tag.Header.DefaultValue,
				},
			}
		case *telemetrypb.Tracing_CustomTag_Literal:
			t.Type = &tracing.CustomTag_Literal_{
				Literal: &tracing.CustomTag_Literal{
					Value: tag.Literal.Value,
				},
			}
		case *telemetrypb.Tracing_CustomTag_Formatter:
			hasFormatterTag = true
			if !supportFormatterTag {
				continue
			}
			t.Type = &tracing.CustomTag_Value{
				Value: tag.Formatter.Value,
			}
		}
		tags = append(tags, t)
	}

	if !supportFormatterTag && hasFormatterTag {
		log.Debug("Formatter custom tag only support for Istio 1.29+")
	}

	return tags
}

func buildCustomTagsFromProxyConfig(customTags map[string]*meshconfig.Tracing_CustomTag) []*tracing.CustomTag {
	var tags []*tracing.CustomTag

	for tagName, tagInfo := range customTags {
		if tagInfo == nil {
			log.Warnf("while building custom tags from proxyConfig, encountered nil custom tag: %s, skipping", tagName)
			continue
		}
		switch tag := tagInfo.Type.(type) {
		case *meshconfig.Tracing_CustomTag_Environment:
			env := &tracing.CustomTag{
				Tag: tagName,
				Type: &tracing.CustomTag_Environment_{
					Environment: &tracing.CustomTag_Environment{
						Name:         tag.Environment.Name,
						DefaultValue: tag.Environment.DefaultValue,
					},
				},
			}
			tags = append(tags, env)
		case *meshconfig.Tracing_CustomTag_Header:
			header := &tracing.CustomTag{
				Tag: tagName,
				Type: &tracing.CustomTag_RequestHeader{
					RequestHeader: &tracing.CustomTag_Header{
						Name:         tag.Header.Name,
						DefaultValue: tag.Header.DefaultValue,
					},
				},
			}
			tags = append(tags, header)
		case *meshconfig.Tracing_CustomTag_Literal:
			env := &tracing.CustomTag{
				Tag: tagName,
				Type: &tracing.CustomTag_Literal_{
					Literal: &tracing.CustomTag_Literal{
						Value: tag.Literal.Value,
					},
				},
			}
			tags = append(tags, env)
		}
	}
	return tags
}

func buildHTTPHeaders(headers []*meshconfig.MeshConfig_ExtensionProvider_HttpHeader) []*core.HeaderValueOption {
	target := make([]*core.HeaderValueOption, 0, len(headers))
	for _, h := range headers {
		hvo := &core.HeaderValueOption{
			AppendAction: core.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD,
			Header: &core.HeaderValue{
				Key:   h.GetName(),
				Value: getHeaderValue(h),
			},
		}
		target = append(target, hvo)
	}
	return target
}

func buildInitialMetadata(metadata []*meshconfig.MeshConfig_ExtensionProvider_HttpHeader) []*core.HeaderValue {
	if metadata == nil {
		return nil
	}
	target := make([]*core.HeaderValue, 0, len(metadata))
	for _, h := range metadata {
		hv := &core.HeaderValue{
			Key:   h.GetName(),
			Value: getHeaderValue(h),
		}
		target = append(target, hv)
	}
	return target
}

// otelServiceName computes the service.name following the OpenTelemetry semantic
// conventions for Kubernetes service attributes:
// https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#service-attributes
//
// The fallback chain is:
//  1. resource.opentelemetry.io/service.name annotation on the pod
//  2. app.kubernetes.io/instance label
//  3. app.kubernetes.io/name label
//  4. Name of the owning Kubernetes resource (Deployment, StatefulSet, etc.)
//  5. Pod name
//  6. Container name (if single container in the pod)
//  7. unknown_service
func otelServiceName(proxy *model.Proxy) string {
	if proxy == nil || proxy.Metadata == nil {
		return "unknown_service"
	}
	meta := proxy.Metadata

	// 1. resource.opentelemetry.io/service.name annotation
	if v, ok := meta.Annotations["resource.opentelemetry.io/service.name"]; ok && v != "" {
		return v
	}

	// 2. app.kubernetes.io/instance label
	if v, ok := proxy.Labels["app.kubernetes.io/instance"]; ok && v != "" {
		return v
	}

	// 3. app.kubernetes.io/name label
	if v, ok := proxy.Labels["app.kubernetes.io/name"]; ok && v != "" {
		return v
	}

	// 4. Name of the owning Kubernetes resource (Deployment, StatefulSet, etc.)
	if meta.WorkloadName != "" {
		return meta.WorkloadName
	}

	// 5. Pod name (extracted from proxy ID which has format "podName.namespace")
	if proxy.ID != "" {
		if podName, _, ok := strings.Cut(proxy.ID, "."); ok && podName != "" {
			return podName
		}
	}

	// 6. Container name (if single container in the pod)
	// AppContainers is on BootstrapNodeMetadata; access via Raw metadata.
	if raw := meta.Raw; raw != nil {
		if containers, ok := raw["APP_CONTAINERS"].(string); ok && containers != "" {
			parts := strings.Split(containers, ",")
			if len(parts) == 1 {
				return parts[0]
			}
		}
	}

	// 7. unknown_service
	return "unknown_service"
}

func getHeaderValue(header *meshconfig.MeshConfig_ExtensionProvider_HttpHeader) string {
	switch hv := header.HeaderValue.(type) {
	case *meshconfig.MeshConfig_ExtensionProvider_HttpHeader_Value:
		return hv.Value
	case *meshconfig.MeshConfig_ExtensionProvider_HttpHeader_EnvName:
		return env.Register[string](hv.EnvName, "", "").Get()
	}
	return ""
}
