// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package configentry

import (
	"fmt"

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

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

type StateStore interface {
	ReadResolvedServiceConfigEntries(memdb.WatchSet, string, *acl.EnterpriseMeta, []structs.ServiceID, structs.ProxyMode) (uint64, *ResolvedServiceConfigSet, error)
}

// MergeNodeServiceWithCentralConfig merges a service instance (NodeService) with the
// proxy-defaults/global and service-defaults/:service config entries.
// This common helper is used by the blocking query function of different RPC endpoints
// that need to return a fully resolved service defintion.
func MergeNodeServiceWithCentralConfig(
	ws memdb.WatchSet,
	state StateStore,
	ns *structs.NodeService,
	logger hclog.Logger) (uint64, *structs.NodeService, error) {

	serviceName := ns.Service
	var upstreams []structs.PeeredServiceName
	if ns.IsSidecarProxy() {
		// This is a sidecar proxy, ignore the proxy service's config since we are
		// managed by the target service config.
		serviceName = ns.Proxy.DestinationServiceName

		// Also if we have any upstreams defined, add them to the defaults lookup request
		// so we can learn about their configs.
		for _, us := range ns.Proxy.Upstreams {
			if us.DestinationType == "" || us.DestinationType == structs.UpstreamDestTypeService {
				psn := us.DestinationID()
				if psn.Peer == "" {
					psn.ServiceName.EnterpriseMeta.Merge(&ns.EnterpriseMeta)
				} else {
					// Peer services should not have their namespace overwritten.
					psn.ServiceName.EnterpriseMeta.OverridePartition(ns.EnterpriseMeta.PartitionOrDefault())
				}
				upstreams = append(upstreams, psn)
			}
		}
	}

	configReq := &structs.ServiceConfigRequest{
		Name:                 serviceName,
		MeshGateway:          ns.Proxy.MeshGateway,
		Mode:                 ns.Proxy.Mode,
		UpstreamServiceNames: upstreams,
		EnterpriseMeta:       ns.EnterpriseMeta,
	}

	// prefer using this vs directly calling the ConfigEntry.ResolveServiceConfig RPC
	// so as to pass down the same watch set to also watch on changes to
	// proxy-defaults/global and service-defaults.
	cfgIndex, configEntries, err := state.ReadResolvedServiceConfigEntries(
		ws,
		configReq.Name,
		&configReq.EnterpriseMeta,
		configReq.GetLocalUpstreamIDs(),
		configReq.Mode,
	)
	if err != nil {
		return 0, nil, fmt.Errorf("Failure looking up service config entries for %s: %v",
			ns.ID, err)
	}

	defaults, err := ComputeResolvedServiceConfig(
		configReq,
		configEntries,
		logger,
	)
	if err != nil {
		return 0, nil, fmt.Errorf("Failure computing service defaults for %s: %v",
			ns.ID, err)
	}

	mergedns, err := MergeServiceConfig(defaults, ns)
	if err != nil {
		return 0, nil, fmt.Errorf("Failure merging service definition with config entry defaults for %s: %v",
			ns.ID, err)
	}

	return cfgIndex, mergedns, nil
}

// MergeServiceConfig merges the service into defaults to produce the final effective
// config for the specified service.
func MergeServiceConfig(defaults *structs.ServiceConfigResponse, service *structs.NodeService) (*structs.NodeService, error) {
	if defaults == nil {
		return service, nil
	}

	// We don't want to change s.registration in place since it is our source of
	// truth about what was actually registered before defaults applied. So copy
	// it first.
	nsRaw, err := copystructure.Copy(service)
	if err != nil {
		return nil, err
	}

	// Merge proxy defaults
	ns := nsRaw.(*structs.NodeService)

	if err := mergo.Merge(&ns.Proxy.Config, defaults.ProxyConfig); err != nil {
		return nil, err
	}
	if err := mergo.Merge(&ns.Proxy.Expose, defaults.Expose); err != nil {
		return nil, err
	}
	if err := mergo.Merge(&ns.Proxy.AccessLogs, defaults.AccessLogs); err != nil {
		return nil, err
	}

	// defaults.EnvoyExtensions contains the extensions from the proxy defaults config entry followed by extensions from
	// the service defaults config entry. This adds the extensions to structs.NodeService.Proxy which in turn is copied
	// into the proxycfg snapshot to ensure the local service's extensions are accessible from the snapshot.
	//
	// This will replace any existing extensions in the NodeService but that is ok because defaults.EnvoyExtensions
	// should have the latest extensions computed from service defaults and proxy defaults.
	ns.Proxy.EnvoyExtensions = nil
	if len(defaults.EnvoyExtensions) > 0 {
		nsExtensions := make([]structs.EnvoyExtension, len(defaults.EnvoyExtensions))
		for i, ext := range defaults.EnvoyExtensions {
			nsExtensions[i] = structs.EnvoyExtension{
				Name:      ext.Name,
				Required:  ext.Required,
				Arguments: ext.Arguments,
			}
		}
		ns.Proxy.EnvoyExtensions = nsExtensions
	}

	if ns.Proxy.MeshGateway.Mode == structs.MeshGatewayModeDefault {
		ns.Proxy.MeshGateway.Mode = defaults.MeshGateway.Mode
	}
	if ns.Proxy.Mode == structs.ProxyModeDefault {
		ns.Proxy.Mode = defaults.Mode
	}
	if ns.Proxy.TransparentProxy.OutboundListenerPort == 0 {
		ns.Proxy.TransparentProxy.OutboundListenerPort = defaults.TransparentProxy.OutboundListenerPort
	}
	if !ns.Proxy.TransparentProxy.DialedDirectly {
		ns.Proxy.TransparentProxy.DialedDirectly = defaults.TransparentProxy.DialedDirectly
	}

	if ns.Proxy.MutualTLSMode == structs.MutualTLSModeDefault {
		ns.Proxy.MutualTLSMode = defaults.MutualTLSMode
	}

	// remoteUpstreams contains synthetic Upstreams generated from central config (service-defaults.UpstreamConfigs).
	remoteUpstreams := make(map[structs.PeeredServiceName]structs.Upstream)

	// If the arguments did not fully normalize tenancy stuff, take care of that now.
	entMeta := ns.EnterpriseMeta
	entMeta.Normalize()

	for _, us := range defaults.UpstreamConfigs {
		parsed, err := structs.ParseUpstreamConfigNoDefaults(us.Config)
		if err != nil {
			return nil, fmt.Errorf("failed to parse upstream config map for %s: %v", us.Upstream.String(), err)
		}

		// If the defaults did not fully normalize tenancy stuff, take care of
		// that now too.
		psn := us.Upstream // only normalize the copy
		psn.ServiceName.EnterpriseMeta.Normalize()

		// Normalize the partition field specially.
		if psn.Peer != "" {
			psn.ServiceName.OverridePartition(entMeta.PartitionOrDefault())
		}

		remoteUpstreams[psn] = structs.Upstream{
			DestinationNamespace: psn.ServiceName.NamespaceOrDefault(),
			DestinationPartition: psn.ServiceName.PartitionOrDefault(),
			DestinationName:      psn.ServiceName.Name,
			DestinationPeer:      psn.Peer,
			Config:               us.Config,
			MeshGateway:          parsed.MeshGateway,
			CentrallyConfigured:  true,
		}
	}

	// localUpstreams stores the upstreams seen from the local registration so that we can merge in the synthetic entries.
	// In transparent proxy mode ns.Proxy.Upstreams will likely be empty because users do not need to define upstreams explicitly.
	// So to store upstream-specific flags from central config, we add entries to ns.Proxy.Upstreams with those values.
	localUpstreams := make(map[structs.PeeredServiceName]struct{})

	// Merge upstream defaults into the local registration
	for i := range ns.Proxy.Upstreams {
		// Get a pointer not a value copy of the upstream struct
		us := &ns.Proxy.Upstreams[i]
		if us.DestinationType != "" && us.DestinationType != structs.UpstreamDestTypeService {
			continue
		}

		uid := us.DestinationID()

		// Normalize the partition field specially.
		if uid.Peer != "" {
			uid.ServiceName.OverridePartition(entMeta.PartitionOrDefault())
		}

		localUpstreams[uid] = struct{}{}
		remoteCfg, ok := remoteUpstreams[uid]
		if !ok {
			// No config defaults to merge
			continue
		}

		// The local upstream config mode has the highest precedence, so only overwrite when it's set to the default
		if us.MeshGateway.Mode == structs.MeshGatewayModeDefault {
			us.MeshGateway.Mode = remoteCfg.MeshGateway.Mode
		}

		preMergeProtocol, found := us.Config["protocol"]
		// Merge in everything else that is read from the map
		if err := mergo.Merge(&us.Config, remoteCfg.Config); err != nil {
			return nil, err
		}

		// Reset the protocol to its pre-merged version for peering upstreams.
		if us.DestinationPeer != "" {
			if found {
				us.Config["protocol"] = preMergeProtocol
			} else {
				delete(us.Config, "protocol")
			}
		}

		// Delete the mesh gateway key from opaque config since this is the value that was resolved from
		// the servers and NOT the final merged value for this upstream.
		// Note that we use the "mesh_gateway" key and not other variants like "MeshGateway" because
		// UpstreamConfig.MergeInto and ResolveServiceConfig only use "mesh_gateway".
		delete(us.Config, "mesh_gateway")
	}

	// Ensure upstreams present in central config are represented in the local configuration.
	// This does not apply outside of transparent mode because in that situation every possible upstream already exists
	// inside of ns.Proxy.Upstreams.
	if ns.Proxy.Mode == structs.ProxyModeTransparent {
		for id, remote := range remoteUpstreams {
			if _, ok := localUpstreams[id]; ok {
				// Remote upstream is already present locally
				continue
			}

			ns.Proxy.Upstreams = append(ns.Proxy.Upstreams, remote)
		}
	}

	return ns, err
}
