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

package awslambda

import (
	"errors"
	"fmt"

	arn_sdk "github.com/aws/aws-sdk-go/aws/arn"
	envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	envoy_lambda_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3"
	envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
	envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
	"github.com/go-viper/mapstructure/v2"
	pstruct "google.golang.org/protobuf/types/known/structpb"

	"github.com/hashicorp/consul/agent/netutil"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/envoyextensions/extensioncommon"
	"github.com/hashicorp/go-multierror"
)

var _ extensioncommon.BasicExtension = (*awsLambda)(nil)

type awsLambda struct {
	extensioncommon.BasicExtensionAdapter

	ARN                string
	PayloadPassthrough bool
	InvocationMode     string
}

// Constructor follows a specific function signature required for the extension registration.
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
	var a awsLambda
	if name := ext.Name; name != api.BuiltinAWSLambdaExtension {
		return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinAWSLambdaExtension, name)
	}
	if err := a.fromArguments(ext.Arguments); err != nil {
		return nil, err
	}
	return &extensioncommon.UpstreamEnvoyExtender{
		Extension: &a,
	}, nil
}

func (a *awsLambda) fromArguments(args map[string]interface{}) error {
	if err := mapstructure.Decode(args, a); err != nil {
		return fmt.Errorf("error decoding extension arguments: %v", err)
	}
	return a.validate()
}

func (a *awsLambda) validate() error {
	var resultErr error
	if a.ARN == "" {
		resultErr = multierror.Append(resultErr, fmt.Errorf("ARN is required"))
	}
	return resultErr
}

// CanApply returns true if the kind of the provided ExtensionConfiguration matches
// the kind of the lambda configuration
func (a *awsLambda) CanApply(config *extensioncommon.RuntimeConfig) bool {
	return config.Kind == config.UpstreamOutgoingProxyKind()
}

// PatchRoute modifies the routing configuration for a service of kind TerminatingGateway. If the kind is
// not TerminatingGateway, then it can not be modified.
func (a *awsLambda) PatchRoute(p extensioncommon.RoutePayload) (*envoy_route_v3.RouteConfiguration, bool, error) {
	cfg := p.RuntimeConfig
	if cfg.Kind != api.ServiceKindTerminatingGateway {
		return p.Message, false, nil
	}

	// Only patch outbound routes.
	if p.IsInbound() {
		return p.Message, false, nil
	}

	route := p.Message

	for _, virtualHost := range route.VirtualHosts {
		for _, route := range virtualHost.Routes {
			action, ok := route.Action.(*envoy_route_v3.Route_Route)

			if !ok {
				continue
			}

			// When auto_host_rewrite is set it conflicts with strip_any_host_port
			// on the http_connection_manager filter, which is required to be true to support
			// lambda functions. See the patch filter method for more details.
			action.Route.HostRewriteSpecifier = nil
		}
	}

	return route, true, nil
}

// PatchCluster patches the provided envoy cluster with data required to support an AWS lambda function
func (a *awsLambda) PatchCluster(p extensioncommon.ClusterPayload) (*envoy_cluster_v3.Cluster, bool, error) {
	// Only patch outbound clusters.
	if p.IsInbound() {
		return p.Message, false, nil
	}

	transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
		Sni: "*.amazonaws.com",
	})

	c := p.Message

	if err != nil {
		return c, false, fmt.Errorf("failed to make transport socket: %w", err)
	}

	// Use the aws SDK to parse the ARN so that we can later extract the region
	parsedARN, err := arn_sdk.Parse(a.ARN)
	if err != nil {
		return c, false, err
	}
	dlf := envoy_cluster_v3.Cluster_V4_ONLY
	ds, _ := netutil.IsDualStack(nil, true)
	if ds {
		dlf = envoy_cluster_v3.Cluster_ALL
	}
	cluster := &envoy_cluster_v3.Cluster{
		Name:                 c.Name,
		ConnectTimeout:       c.ConnectTimeout,
		ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_LOGICAL_DNS},
		DnsLookupFamily:      dlf,
		LbPolicy:             envoy_cluster_v3.Cluster_ROUND_ROBIN,
		Metadata: &envoy_core_v3.Metadata{
			FilterMetadata: map[string]*pstruct.Struct{
				"com.amazonaws.lambda": {
					Fields: map[string]*pstruct.Value{
						"egress_gateway": {Kind: &pstruct.Value_BoolValue{BoolValue: true}},
					},
				},
			},
		},
		LoadAssignment: &envoy_endpoint_v3.ClusterLoadAssignment{
			ClusterName: c.Name,
			Endpoints: []*envoy_endpoint_v3.LocalityLbEndpoints{
				{
					LbEndpoints: []*envoy_endpoint_v3.LbEndpoint{
						{
							HostIdentifier: &envoy_endpoint_v3.LbEndpoint_Endpoint{
								Endpoint: &envoy_endpoint_v3.Endpoint{
									Address: &envoy_core_v3.Address{
										Address: &envoy_core_v3.Address_SocketAddress{
											SocketAddress: &envoy_core_v3.SocketAddress{
												Address: fmt.Sprintf("lambda.%s.amazonaws.com", parsedARN.Region),
												PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
													PortValue: 443,
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		TransportSocket: transportSocket,
	}
	return cluster, true, nil
}

// PatchFilter patches the provided envoy filter with an inserted lambda filter being careful not to
// overwrite the http filters.
func (a *awsLambda) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener_v3.Filter, bool, error) {
	filter := p.Message

	// Only patch outbound filters.
	if p.IsInbound() {
		return filter, false, nil
	}

	if filter.Name != "envoy.filters.network.http_connection_manager" {
		return filter, false, nil
	}
	if typedConfig := filter.GetTypedConfig(); typedConfig == nil {
		return filter, false, errors.New("error getting typed config for http filter")
	}

	config := envoy_resource_v3.GetHTTPConnectionManager(filter)
	if config == nil {
		return filter, false, errors.New("error unmarshalling filter")
	}

	lambdaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
		"envoy.filters.http.aws_lambda",
		&envoy_lambda_v3.Config{
			Arn:                a.ARN,
			PayloadPassthrough: a.PayloadPassthrough,
			InvocationMode:     toEnvoyInvocationMode(a.InvocationMode),
		},
	)
	if err != nil {
		return filter, false, err
	}

	// We need to be careful about overwriting http filters completely because
	// http filters validates intentions with the RBAC filter. This inserts the
	// lambda filter before `envoy.filters.http.router` while keeping everything
	// else intact.
	changedFilters := make([]*envoy_http_v3.HttpFilter, 0, len(config.HttpFilters)+1)
	var changed bool
	for _, httpFilter := range config.HttpFilters {
		if httpFilter.Name == "envoy.filters.http.router" {
			changedFilters = append(changedFilters, lambdaHttpFilter)
			changed = true
		}
		changedFilters = append(changedFilters, httpFilter)
	}
	if changed {
		config.HttpFilters = changedFilters
	}

	// StripPortMode must be set to true since all requests have to be signed using the AWS v4 signature and
	// if the port is included in the request, it will be used in the signature calculation causing AWS to reject the
	// Lambda HTTP request.
	config.StripPortMode = &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{
		StripAnyHostPort: true,
	}
	newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config)
	if err != nil {
		return filter, false, errors.New("error making new filter")
	}

	return newFilter, true, nil
}

func toEnvoyInvocationMode(s string) envoy_lambda_v3.Config_InvocationMode {
	m := envoy_lambda_v3.Config_SYNCHRONOUS
	if s == "asynchronous" {
		m = envoy_lambda_v3.Config_ASYNCHRONOUS
	}
	return m
}
