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

package configentry

import (
	"fmt"

	"github.com/hashicorp/go-hclog"
	"github.com/imdario/mergo"
	"github.com/mitchellh/copystructure"

	"github.com/hashicorp/consul/agent/structs"
)

func ComputeResolvedServiceConfig(
	args *structs.ServiceConfigRequest,
	entries *ResolvedServiceConfigSet,
	logger hclog.Logger,
) (*structs.ServiceConfigResponse, error) {
	var thisReply structs.ServiceConfigResponse

	thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault

	// Store the upstream defaults under a wildcard key so that they can be applied to
	// upstreams that are inferred from intentions and do not have explicit upstream configuration.
	wildcard := structs.PeeredServiceName{
		ServiceName: structs.NewServiceName(structs.WildcardSpecifier, args.WithWildcardNamespace()),
	}
	wildcardUpstreamDefaults := make(map[string]interface{})
	// resolvedConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID.
	resolvedConfigs := make(map[structs.PeeredServiceName]map[string]interface{})

	// TODO(freddy) Refactor this into smaller set of state store functions
	// Pass the WatchSet to both the service and proxy config lookups. If either is updated during the
	// blocking query, this function will be rerun and these state store lookups will both be current.
	// We use the default enterprise meta to look up the global proxy defaults because they are not namespaced.

	var proxyConfGlobalProtocol string
	proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault())
	if proxyConf != nil {
		// Apply the proxy defaults to the sidecar's proxy config
		mapCopy, err := copystructure.Copy(proxyConf.Config)
		if err != nil {
			return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err)
		}

		thisReply.ProxyConfig = mapCopy.(map[string]interface{})
		thisReply.Mode = proxyConf.Mode
		thisReply.TransparentProxy = proxyConf.TransparentProxy
		thisReply.MutualTLSMode = proxyConf.MutualTLSMode
		thisReply.MeshGateway = proxyConf.MeshGateway
		thisReply.Expose = proxyConf.Expose
		thisReply.EnvoyExtensions = proxyConf.EnvoyExtensions
		thisReply.AccessLogs = proxyConf.AccessLogs

		// Only MeshGateway and Protocol should affect upstreams.
		// MeshGateway is strange. It's marshaled into UpstreamConfigs via the arbitrary map, but it
		// uses concrete fields everywhere else. We always take the explicit definition here for
		// wildcard upstreams and discard the user setting it via arbitrary map in proxy-defaults.
		if mgw, ok := thisReply.ProxyConfig["mesh_gateway"]; ok {
			wildcardUpstreamDefaults["mesh_gateway"] = mgw
		}
		if !proxyConf.MeshGateway.IsZero() {
			wildcardUpstreamDefaults["mesh_gateway"] = proxyConf.MeshGateway
		}

		// We explicitly DO NOT merge the protocol from proxy-defaults into the wildcard upstream here.
		// TProxy will try to use the data from the `wildcardUpstreamDefaults` as a source of truth, which is
		// normally correct to inherit from proxy-defaults. However, it is NOT correct for protocol.
		//
		// This edge-case is different for `protocol` from other fields, since the protocol can be
		// set on both the local `ServiceDefaults.UpstreamOverrides` and upstream `ServiceDefaults.Protocol`.
		// This means that when proxy-defaults is set, it would always be treated as an explicit override,
		// and take precedence over the protocol that is set on the discovery chain (which comes from the
		// service's preference in its service-defaults), which is wrong.
		//
		// When the upstream is not explicitly defined, we should only get the protocol from one of these locations:
		//   1. For tproxy non-peering services, it can be fetched via the discovery chain.
		//      The chain compiler merges the proxy-defaults protocol with the upstream's preferred service-defaults protocol.
		//   2. For tproxy non-peering services with default upstream overrides, it will come from the wildcard upstream overrides.
		//   3. For tproxy non-peering services with specific upstream overrides, it will come from the specific upstream override defined.
		//   4. For tproxy peering services, they do not honor the proxy-defaults, since they reside in a different cluster.
		//      The data will come from a separate peerMeta field.
		// In all of these cases, it is not necessary for the proxy-defaults to exist in the wildcard upstream.
		parsed, err := structs.ParseUpstreamConfigNoDefaults(mapCopy.(map[string]interface{}))
		if err != nil {
			return nil, fmt.Errorf("failed to parse upstream config map for proxy-defaults: %v", err)
		}
		proxyConfGlobalProtocol = parsed.Protocol
	}

	serviceConf := entries.GetServiceDefaults(
		structs.NewServiceID(args.Name, &args.EnterpriseMeta),
	)
	if serviceConf != nil {

		if serviceConf.Expose.Checks {
			thisReply.Expose.Checks = true
		}
		if len(serviceConf.Expose.Paths) >= 1 {
			thisReply.Expose.Paths = serviceConf.Expose.Paths
		}
		if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault {
			thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode
			wildcardUpstreamDefaults["mesh_gateway"] = serviceConf.MeshGateway
		}
		if serviceConf.TransparentProxy.OutboundListenerPort != 0 {
			thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort
		}
		if serviceConf.TransparentProxy.DialedDirectly {
			thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly
		}
		if serviceConf.Mode != structs.ProxyModeDefault {
			thisReply.Mode = serviceConf.Mode
		}
		if serviceConf.Destination != nil {
			thisReply.Destination = *serviceConf.Destination
		}
		if serviceConf.RateLimits != nil {
			thisReply.RateLimits = *serviceConf.RateLimits
		}

		// Populate values for the proxy config map
		proxyConf := thisReply.ProxyConfig
		if proxyConf == nil {
			proxyConf = make(map[string]interface{})
		}
		if serviceConf.Protocol != "" {
			proxyConf["protocol"] = serviceConf.Protocol
		}
		if serviceConf.BalanceInboundConnections != "" {
			proxyConf["balance_inbound_connections"] = serviceConf.BalanceInboundConnections
		}
		if serviceConf.MaxInboundConnections > 0 {
			proxyConf["max_inbound_connections"] = serviceConf.MaxInboundConnections
		}
		if serviceConf.LocalConnectTimeoutMs > 0 {
			proxyConf["local_connect_timeout_ms"] = serviceConf.LocalConnectTimeoutMs
		}
		if serviceConf.LocalRequestTimeoutMs > 0 {
			proxyConf["local_request_timeout_ms"] = serviceConf.LocalRequestTimeoutMs
		}
		// Add the proxy conf to the response if any fields were populated
		if len(proxyConf) > 0 {
			thisReply.ProxyConfig = proxyConf
		}

		if serviceConf.MutualTLSMode != structs.MutualTLSModeDefault {
			thisReply.MutualTLSMode = serviceConf.MutualTLSMode
		}

		thisReply.Meta = serviceConf.Meta
		// Service defaults' envoy extensions are appended to the proxy defaults extensions so that proxy defaults
		// extensions are applied first.
		thisReply.EnvoyExtensions = append(thisReply.EnvoyExtensions, serviceConf.EnvoyExtensions...)
	}

	// First collect all upstreams into a set of seen upstreams.
	// Upstreams can come from:
	// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
	// - Implicitly from centralized upstream config in service-defaults
	seenUpstreams := map[structs.PeeredServiceName]struct{}{}

	var (
		noUpstreamArgs = len(args.UpstreamServiceNames) == 0

		// Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode
		// will never be transparent because the service config request does not use the resolved value.
		tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent
	)

	// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
	// If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode.
	// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
	if noUpstreamArgs && !tproxy {
		return &thisReply, nil
	}

	// First store all upstreams that were provided in the request
	for _, psn := range args.UpstreamServiceNames {
		if _, ok := seenUpstreams[psn]; !ok {
			seenUpstreams[psn] = struct{}{}
		}
	}

	// Then store upstreams inferred from service-defaults and mapify the overrides.
	var (
		upstreamDefaults  *structs.UpstreamConfig
		upstreamOverrides = make(map[structs.PeeredServiceName]*structs.UpstreamConfig)
	)
	if serviceConf != nil && serviceConf.UpstreamConfig != nil {
		for i, override := range serviceConf.UpstreamConfig.Overrides {
			if override.Name == "" {
				logger.Warn(
					"Skipping UpstreamConfig.Overrides entry without a required name field",
					"entryIndex", i,
					"kind", serviceConf.GetKind(),
					"name", serviceConf.GetName(),
					"namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(),
				)
				continue // skip this impossible condition
			}
			psn := override.PeeredServiceName()
			seenUpstreams[psn] = struct{}{}
			upstreamOverrides[psn] = override
		}
		if serviceConf.UpstreamConfig.Defaults != nil {
			upstreamDefaults = serviceConf.UpstreamConfig.Defaults
			if upstreamDefaults.MeshGateway.Mode == structs.MeshGatewayModeDefault {
				upstreamDefaults.MeshGateway.Mode = thisReply.MeshGateway.Mode
			}
			upstreamDefaults.MergeInto(wildcardUpstreamDefaults)
			// Always add the wildcard upstream if a service-defaults default-upstream was configured.
			resolvedConfigs[wildcard] = wildcardUpstreamDefaults
		}
	}

	if !args.MeshGateway.IsZero() {
		wildcardUpstreamDefaults["mesh_gateway"] = args.MeshGateway
	}

	// Add the wildcard upstream if any fields were populated (it may have been already
	// added if a service-defaults exists). We likely could always add it without issues,
	// but this has been existing behavior, and many unit tests would break.
	if len(wildcardUpstreamDefaults) > 0 {
		resolvedConfigs[wildcard] = wildcardUpstreamDefaults
	}

	for upstream := range seenUpstreams {
		resolvedCfg := make(map[string]interface{})

		// The protocol of an upstream is resolved in this order:
		// 1. Default protocol from proxy-defaults (how all services should be addressed)
		// 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed)
		// 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream
		// 	  (how the downstream wants to address it)
		if proxyConfGlobalProtocol != "" {
			resolvedCfg["protocol"] = proxyConfGlobalProtocol
		}

		if err := mergo.MergeWithOverwrite(&resolvedCfg, wildcardUpstreamDefaults); err != nil {
			return nil, fmt.Errorf("failed to merge wildcard defaults into upstream: %v", err)
		}

		upstreamSvcDefaults := entries.GetServiceDefaults(upstream.ServiceName.ToServiceID())
		if upstreamSvcDefaults != nil {
			if upstreamSvcDefaults.Protocol != "" {
				resolvedCfg["protocol"] = upstreamSvcDefaults.Protocol
			}
		}

		// When dialing an upstream, the goal is to flatten the mesh gateway mode in this order
		// (larger number wins):
		//  1. Value from the proxy-defaults
		//  2. Value from top-level of service-defaults (ServiceDefaults.MeshGateway)
		//  3. Value from centralized upstream defaults (ServiceDefaults.UpstreamConfig.Defaults)
		//  4. Value from local proxy registration (NodeService.Proxy.MeshGateway)
		//  5. Value from centralized upstream override (ServiceDefaults.UpstreamConfig.Overrides)
		//  6. Value from local upstream definition (NodeService.Proxy.Upstreams[].MeshGateway)
		//
		// The MeshGateway value from upstream definitions in the proxy registration override
		// the one from UpstreamConfig.Defaults and UpstreamConfig.Overrides because they are
		// specific to the proxy instance.
		//
		// Step 6 is handled by the dialer's ServiceManager in MergeServiceConfig.

		// Start with the merged value from proxyConf and serviceConf. (steps 1-2)
		if !thisReply.MeshGateway.IsZero() {
			resolvedCfg["mesh_gateway"] = thisReply.MeshGateway
		}

		// Merge in the upstream defaults (step 3).
		if upstreamDefaults != nil {
			upstreamDefaults.MergeInto(resolvedCfg)
		}

		// Merge in the top-level mode from the proxy instance (step 4).
		if !args.MeshGateway.IsZero() {
			// This means each upstream inherits the value from the `NodeService.Proxy.MeshGateway` field.
			resolvedCfg["mesh_gateway"] = args.MeshGateway
		}

		// Merge in Overrides for the upstream (step 5).
		if upstreamOverrides[upstream] != nil {
			upstreamOverrides[upstream].MergeInto(resolvedCfg)
		}

		if len(resolvedCfg) > 0 {
			resolvedConfigs[upstream] = resolvedCfg
		}
	}

	// don't allocate the slices just to not fill them
	if len(resolvedConfigs) == 0 {
		return &thisReply, nil
	}

	thisReply.UpstreamConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(resolvedConfigs))

	for us, conf := range resolvedConfigs {
		thisReply.UpstreamConfigs = append(thisReply.UpstreamConfigs,
			structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
	}

	return &thisReply, nil
}
