// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package extensionruntime

import (
	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/proxycfg"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/envoyextensions/extensioncommon"
)

func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.CompoundServiceName][]extensioncommon.RuntimeConfig {
	extensionsMap := make(map[api.CompoundServiceName][]api.EnvoyExtension)
	upstreamMap := make(map[api.CompoundServiceName]*extensioncommon.UpstreamData)
	var kind api.ServiceKind
	extensionConfigurationsMap := make(map[api.CompoundServiceName][]extensioncommon.RuntimeConfig)

	trustDomain := ""
	if cfgSnap.Roots != nil {
		trustDomain = cfgSnap.Roots.TrustDomain
	}

	switch cfgSnap.Kind {
	case structs.ServiceKindConnectProxy:
		kind = api.ServiceKindConnectProxy
		outgoingKindByService := make(map[api.CompoundServiceName]api.ServiceKind)
		vipForService := make(map[api.CompoundServiceName]string)
		for uid, upstreamData := range cfgSnap.ConnectProxy.WatchedUpstreamEndpoints {
			sn := upstreamIDToCompoundServiceName(uid)

			for _, serviceNodes := range upstreamData {
				for _, serviceNode := range serviceNodes {
					if serviceNode.Service == nil {
						continue
					}
					vip := serviceNode.Service.TaggedAddresses[structs.TaggedAddressVirtualIP].Address
					if vip != "" {
						if _, ok := vipForService[sn]; !ok {
							vipForService[sn] = vip
						}
					}
					// Store the upstream's kind, and for ServiceKindTypical we don't do anything because we'll default
					// any unset upstreams to ServiceKindConnectProxy below.
					switch serviceNode.Service.Kind {
					case structs.ServiceKindTypical:
					default:
						outgoingKindByService[sn] = api.ServiceKind(serviceNode.Service.Kind)
					}
					// We only need the kind from one instance, so break once we find it.
					break
				}
			}
		}

		// TODO(peering): consider PeerUpstreamEndpoints in addition to DiscoveryChain
		// These are the discovery chains for upstreams which have the Envoy Extensions applied to the local service.
		for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain {
			compoundServiceName := upstreamIDToCompoundServiceName(uid)
			extensionsMap[compoundServiceName] = convertEnvoyExtensions(chain.EnvoyExtensions)

			primarySNI := connect.ServiceSNI(uid.Name, "", chain.Namespace, chain.Partition, cfgSnap.Datacenter, trustDomain)
			snis := make(map[string]struct{})
			for _, t := range chain.Targets {
				// SNI isn't set for peered services. We don't support peered services yet.
				if t.SNI != "" {
					snis[t.SNI] = struct{}{}
				}
			}

			outgoingKind, ok := outgoingKindByService[compoundServiceName]
			if !ok {
				outgoingKind = api.ServiceKindConnectProxy
			}

			upstreamMap[compoundServiceName] = &extensioncommon.UpstreamData{
				PrimarySNI:        primarySNI,
				SNIs:              snis,
				VIP:               vipForService[compoundServiceName],
				EnvoyID:           uid.EnvoyID(),
				OutgoingProxyKind: outgoingKind,
			}
		}
		// Adds extensions configured for the local service to the RuntimeConfig. This only applies to
		// connect-proxies because extensions are either global or tied to a specific service, so the terminating
		// gateway's Envoy resources for the local service (i.e not to upstreams) would never need to be modified.
		localSvc := api.CompoundServiceName{
			Name:      cfgSnap.Proxy.DestinationServiceName,
			Namespace: cfgSnap.ProxyID.NamespaceOrDefault(),
			Partition: cfgSnap.ProxyID.PartitionOrEmpty(),
		}
		extensionConfigurationsMap[localSvc] = []extensioncommon.RuntimeConfig{}
		cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions)
		for _, ext := range cfgSnapExts {
			extCfg := extensioncommon.RuntimeConfig{
				EnvoyExtension:        ext,
				ServiceName:           localSvc,
				IsSourcedFromUpstream: false,
				Upstreams:             upstreamMap,
				Kind:                  kind,
				Protocol:              proxyConfigProtocol(cfgSnap.Proxy.Config),
			}
			extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg)
		}
	case structs.ServiceKindTerminatingGateway:
		kind = api.ServiceKindTerminatingGateway
		for svc, c := range cfgSnap.TerminatingGateway.ServiceConfigs {
			compoundServiceName := serviceNameToCompoundServiceName(svc)
			extensionsMap[compoundServiceName] = convertEnvoyExtensions(c.EnvoyExtensions)

			primarySNI := connect.ServiceSNI(svc.Name, "", svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
			envoyID := proxycfg.NewUpstreamIDFromServiceName(svc)

			snis := map[string]struct{}{primarySNI: {}}

			resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc]
			if hasResolver {
				for subsetName := range resolver.Subsets {
					sni := connect.ServiceSNI(svc.Name, subsetName, svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
					snis[sni] = struct{}{}
				}
			}

			upstreamMap[compoundServiceName] = &extensioncommon.UpstreamData{
				PrimarySNI:        primarySNI,
				SNIs:              snis,
				EnvoyID:           envoyID.EnvoyID(),
				OutgoingProxyKind: api.ServiceKindTerminatingGateway,
			}
		}
	case structs.ServiceKindAPIGateway:
		kind = api.ServiceKindAPIGateway
		// For API Gateway, we need to handle the gateway itself
		localSvc := api.CompoundServiceName{
			Name:      cfgSnap.Service,
			Namespace: cfgSnap.ProxyID.NamespaceOrDefault(),
			Partition: cfgSnap.ProxyID.PartitionOrEmpty(),
		}

		// Handle extensions for the API Gateway itself
		extensionConfigurationsMap[localSvc] = []extensioncommon.RuntimeConfig{}
		cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions)
		for _, ext := range cfgSnapExts {
			extCfg := extensioncommon.RuntimeConfig{
				EnvoyExtension:        ext,
				ServiceName:           localSvc,
				IsSourcedFromUpstream: false,
				Upstreams:             upstreamMap,
				Kind:                  kind,
				Protocol:              proxyConfigProtocol(cfgSnap.Proxy.Config),
			}
			extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg)
		}
	}

	// If applicable, include extension configuration for remote upstreams of the local service.
	// This only applies to specific extensions authorized to apply to remote proxies.
	for svc, exts := range extensionsMap {
		extensionConfigurationsMap[svc] = []extensioncommon.RuntimeConfig{}
		for _, ext := range exts {
			if appliesToRemoteDownstreams(ext) {
				extCfg := extensioncommon.RuntimeConfig{
					EnvoyExtension:        ext,
					Kind:                  kind,
					ServiceName:           svc,
					IsSourcedFromUpstream: true,
					Upstreams:             upstreamMap,
					Protocol:              proxyConfigProtocol(cfgSnap.Proxy.Config),
				}
				extensionConfigurationsMap[svc] = append(extensionConfigurationsMap[svc], extCfg)
			}
		}
	}

	return extensionConfigurationsMap
}

func serviceNameToCompoundServiceName(svc structs.ServiceName) api.CompoundServiceName {
	return api.CompoundServiceName{
		Name:      svc.Name,
		Partition: svc.PartitionOrDefault(),
		Namespace: svc.NamespaceOrDefault(),
	}
}

func upstreamIDToCompoundServiceName(uid proxycfg.UpstreamID) api.CompoundServiceName {
	return api.CompoundServiceName{
		Name:      uid.Name,
		Partition: uid.PartitionOrDefault(),
		Namespace: uid.NamespaceOrDefault(),
	}
}

func convertEnvoyExtensions(structExtensions structs.EnvoyExtensions) []api.EnvoyExtension {
	return structExtensions.ToAPI()
}

func proxyConfigProtocol(cfg map[string]any) string {
	if p, exists := cfg["protocol"]; exists {
		if protocol, ok := p.(string); ok {
			return protocol
		}
	}
	return ""
}

// appliesToRemoteDownstreams returns true if the given extension should be applied to remote downstream proxies of the
// service targeted by the extension, rather than just the local proxy. In the context of GetRuntimeConfigurations, this
// determines whether the extension should apply to the local proxy (a downstream of the configured service).
//
// Currently, only the AWS Lambda and Validate extensions are allowed to apply to downstream proxies.
//
// See extensioncommon.RuntimeConfig.IsSourcedFromUpstream and UpstreamEnvoyExtender doc for more information. We make
// this check here out of precaution s.t. even if an unauthorized extension is erroneously constructed with the
// UpstreamEnvoyExtender, this check will not allow the upstream extension configuration to be provided.
func appliesToRemoteDownstreams(extension api.EnvoyExtension) bool {
	return extension.Name == api.BuiltinAWSLambdaExtension || extension.Name == api.BuiltinValidateExtension
}
