package xds

import (
	"errors"
	"fmt"

	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
	envoy_tls_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
	envoy_core_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
	envoy_listener_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
	envoy_route_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
	envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	envoy_grpc_stats_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/grpc_stats/v2alpha"
	envoy_http_rbac_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/rbac/v2"
	envoy_tls_inspector_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/listener/tls_inspector/v2"
	envoy_http_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
	envoy_network_rbac_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rbac/v2"
	envoy_sni_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/sni_cluster/v2"
	envoy_tcp_proxy_v2 "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/tcp_proxy/v2"
	envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	envoy_metrics_v2 "github.com/envoyproxy/go-control-plane/envoy/config/metrics/v2"
	envoy_metrics_v3 "github.com/envoyproxy/go-control-plane/envoy/config/metrics/v3"
	envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	envoy_trace_v2 "github.com/envoyproxy/go-control-plane/envoy/config/trace/v2"
	envoy_trace_v3 "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3"
	envoy_grpc_stats_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_stats/v3"
	envoy_http_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
	envoy_tls_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
	envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	envoy_network_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
	envoy_sni_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_cluster/v3"
	envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
	envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
	envoy_discovery_v2 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
	envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	envoy_type_v2 "github.com/envoyproxy/go-control-plane/envoy/type"
	envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"github.com/golang/protobuf/ptypes/any"
	"google.golang.org/grpc"
)

// The plumbing in this file supports converting xDS v3 requests and responses
// to and from xDS v2 representations. This is primarily of use for envoy
// sidecars configured and launched by Consul versions prior to 1.10.
//
// Once the servers and client agents in a datacenter have been upgraded to
// 1.10+, and all of the sidecars have been restarted with a fresh bootstrap
// config file generated by Consul 1.10+ then none of these abstractions are
// used.
//
// At most we only need to retain this logic until our envoy support matrix
// looks like:
//
// - 1.20.x (v2 deleted)
// - 1.19.x (v2 deleted)
// - 1.18.x (v2 deleted)
// - 1.17.x (v2 opt-in)

type adsServerV2Shim struct {
	srv *Server
}

// StreamAggregatedResources implements
// envoy_discovery_v2.AggregatedDiscoveryServiceServer. This is the ADS endpoint which is
// the only xDS API we directly support for now.
func (s *adsServerV2Shim) StreamAggregatedResources(stream ADSStream_v2) error {
	shim := &adsStreamV3Shim{
		stream:       stream,
		ServerStream: stream,
	}
	return s.srv.streamAggregatedResources(shim)
}

// DeltaAggregatedResources implements envoy_discovery_v2.AggregatedDiscoveryServiceServer
func (s *adsServerV2Shim) DeltaAggregatedResources(_ envoy_discovery_v2.AggregatedDiscoveryService_DeltaAggregatedResourcesServer) error {
	return errors.New("not implemented")
}

type adsStreamV3Shim struct {
	stream ADSStream_v2
	grpc.ServerStream
}

var _ ADSStream = (*adsStreamV3Shim)(nil)

func (s *adsStreamV3Shim) Send(resp *envoy_discovery_v3.DiscoveryResponse) error {
	respv2, err := convertDiscoveryResponseToV2(resp)
	if err != nil {
		return fmt.Errorf("Error converting a v3 DiscoveryResponse to v2: %w", err)
	}

	return s.stream.Send(respv2)
}

func (s *adsStreamV3Shim) Recv() (*envoy_discovery_v3.DiscoveryRequest, error) {
	req, err := s.stream.Recv()
	if err != nil {
		return nil, err
	}

	reqv3, err := convertDiscoveryRequestToV3(req)
	if err != nil {
		return nil, fmt.Errorf("Error converting a v2 DiscoveryRequest to v3: %w", err)
	}

	return reqv3, nil
}

func convertDiscoveryRequestToV3(req *envoy_api_v2.DiscoveryRequest) (*envoy_discovery_v3.DiscoveryRequest, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(req); err != nil {
		return nil, err
	}

	var reqV3 envoy_discovery_v3.DiscoveryRequest
	if err := pbuf.Unmarshal(&reqV3); err != nil {
		return nil, err
	}

	// only one field to munge
	if err := convertTypeUrlsToV3(&reqV3.TypeUrl); err != nil {
		return nil, err
	}

	return &reqV3, nil
}

// convertClusterToV2 is only used in tests.
func convertClusterToV2(resp *envoy_cluster_v3.Cluster) (*envoy_api_v2.Cluster, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(resp); err != nil {
		return nil, err
	}

	var v2 envoy_api_v2.Cluster
	if err := pbuf.Unmarshal(&v2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&v2); err != nil {
		return nil, err
	}

	return &v2, nil
}

// convertClusterLoadAssignmentToV2 is only used in tests.
func convertClusterLoadAssignmentToV2(resp *envoy_endpoint_v3.ClusterLoadAssignment) (*envoy_api_v2.ClusterLoadAssignment, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(resp); err != nil {
		return nil, err
	}

	var v2 envoy_api_v2.ClusterLoadAssignment
	if err := pbuf.Unmarshal(&v2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&v2); err != nil {
		return nil, err
	}

	return &v2, nil
}

// convertRouteConfigurationToV2 is only used in tests.
func convertRouteConfigurationToV2(resp *envoy_route_v3.RouteConfiguration) (*envoy_api_v2.RouteConfiguration, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(resp); err != nil {
		return nil, err
	}

	var v2 envoy_api_v2.RouteConfiguration
	if err := pbuf.Unmarshal(&v2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&v2); err != nil {
		return nil, err
	}

	return &v2, nil
}

// convertListenerToV2 is only used in tests.
func convertListenerToV2(resp *envoy_listener_v3.Listener) (*envoy_api_v2.Listener, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(resp); err != nil {
		return nil, err
	}

	var v2 envoy_api_v2.Listener
	if err := pbuf.Unmarshal(&v2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&v2); err != nil {
		return nil, err
	}

	return &v2, nil
}

func convertDiscoveryResponseToV2(resp *envoy_discovery_v3.DiscoveryResponse) (*envoy_api_v2.DiscoveryResponse, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(resp); err != nil {
		return nil, err
	}

	var respV2 envoy_api_v2.DiscoveryResponse
	if err := pbuf.Unmarshal(&respV2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&respV2); err != nil {
		return nil, err
	}

	return &respV2, nil
}

// convertNetFilterToV2 is only used in tests.
func convertNetFilterToV2(filter *envoy_listener_v3.Filter) (*envoy_listener_v2.Filter, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(filter); err != nil {
		return nil, err
	}

	var filterV2 envoy_listener_v2.Filter
	if err := pbuf.Unmarshal(&filterV2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&filterV2); err != nil {
		return nil, err
	}

	return &filterV2, nil
}

// convertHttpFilterToV2 is only used in tests.
func convertHttpFilterToV2(filter *envoy_http_v3.HttpFilter) (*envoy_http_v2.HttpFilter, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(filter); err != nil {
		return nil, err
	}

	var filterV2 envoy_http_v2.HttpFilter
	if err := pbuf.Unmarshal(&filterV2); err != nil {
		return nil, err
	}

	if err := convertTypedConfigsToV2(&filterV2); err != nil {
		return nil, err
	}

	return &filterV2, nil
}

func convertSemanticVersionToV2(version *envoy_type_v3.SemanticVersion) (*envoy_type_v2.SemanticVersion, error) {
	var pbuf proto.Buffer
	if err := pbuf.Marshal(version); err != nil {
		return nil, err
	}

	var versionV2 envoy_type_v2.SemanticVersion
	if err := pbuf.Unmarshal(&versionV2); err != nil {
		return nil, err
	}

	return &versionV2, nil
}

// Responses
func convertTypedConfigsToV2(pb proto.Message) error {
	switch x := pb.(type) {
	case *envoy_api_v2.DiscoveryResponse:
		if err := convertTypeUrlsToV2(&x.TypeUrl); err != nil {
			return fmt.Errorf("%T: %w", x, err)
		}
		for _, res := range x.Resources {
			if err := convertTypedConfigsToV2(res); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *any.Any:
		// first flip the any.Any to v2
		if err := convertTypeUrlsToV2(&x.TypeUrl); err != nil {
			return fmt.Errorf("%T(%s) convert type urls in envelope: %w", x, x.TypeUrl, err)
		}

		// now decode into a v2 type
		var dynAny ptypes.DynamicAny
		if err := ptypes.UnmarshalAny(x, &dynAny); err != nil {
			return fmt.Errorf("%T(%s) dynamic unmarshal: %w", x, x.TypeUrl, err)
		}

		// handle the contents and then put them back in the any.Any
		// handle contents first
		if err := convertTypedConfigsToV2(dynAny.Message); err != nil {
			return fmt.Errorf("%T(%s) convert type urls in body: %w", x, x.TypeUrl, err)
		}
		anyFixed, err := ptypes.MarshalAny(dynAny.Message)
		if err != nil {
			return fmt.Errorf("%T(%s) dynamic re-marshal: %w", x, x.TypeUrl, err)
		}
		x.Value = anyFixed.Value
		return nil
	case *envoy_api_v2.Listener:
		for _, chain := range x.FilterChains {
			if err := convertTypedConfigsToV2(chain); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		for _, filter := range x.ListenerFilters {
			// We only ever plumb up the tls_inspector listener filter.
			if err := convertTypedConfigsToV2(filter); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *envoy_listener_v2.ListenerFilter:
		// This is really only here for when the tls inspector is for some
		// random reason plumbed using the @type instead of the name.
		if x.ConfigType != nil {
			tc, ok := x.ConfigType.(*envoy_listener_v2.ListenerFilter_TypedConfig)
			if !ok {
				return fmt.Errorf("%T: ConfigType type %T not handled", x, x.ConfigType)
			}
			if tc.TypedConfig != nil {
				if err := convertTypedConfigsToV2(tc.TypedConfig); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		return nil
	case *envoy_listener_v2.FilterChain:
		for _, filter := range x.Filters {
			if err := convertTypedConfigsToV2(filter); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		if x.TransportSocket != nil {
			if err := convertTypedConfigsToV2(x.TransportSocket); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *envoy_listener_v2.Filter:
		// "envoy.filters.network.tcp_proxy"
		// "envoy.filters.network.http_connection_manager"
		// "envoy.filters.network.rbac"
		// "envoy.filters.network.sni_cluster"
		if x.ConfigType != nil {
			tc, ok := x.ConfigType.(*envoy_listener_v2.Filter_TypedConfig)
			if !ok {
				return fmt.Errorf("%T: ConfigType type %T not handled", x, x.ConfigType)
			}
			if tc.TypedConfig != nil {
				if err := convertTypedConfigsToV2(tc.TypedConfig); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		return nil
	case *envoy_core_v2.TransportSocket:
		if x.ConfigType != nil {
			tc, ok := x.ConfigType.(*envoy_core_v2.TransportSocket_TypedConfig)
			if !ok {
				return fmt.Errorf("%T: ConfigType type %T not handled", x, x.ConfigType)
			}
			if tc.TypedConfig != nil {
				if err := convertTypedConfigsToV2(tc.TypedConfig); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		return nil
	case *envoy_api_v2.ClusterLoadAssignment:
		return nil
	case *envoy_api_v2.Cluster:
		if x.TransportSocket != nil {
			if err := convertTypedConfigsToV2(x.TransportSocket); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		for _, tsm := range x.TransportSocketMatches {
			if tsm.TransportSocket != nil {
				if err := convertTypedConfigsToV2(tsm.TransportSocket); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		if x.EdsClusterConfig != nil {
			if x.EdsClusterConfig.EdsConfig != nil {
				if err := convertTypedConfigsToV2(x.EdsClusterConfig.EdsConfig); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		return nil
	case *envoy_api_v2.RouteConfiguration:
		for _, vhost := range x.VirtualHosts {
			if err := convertTypedConfigsToV2(vhost); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		if x.Vhds != nil && x.Vhds.ConfigSource != nil {
			if err := convertTypedConfigsToV2(x.Vhds.ConfigSource); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *envoy_route_v2.VirtualHost:
		if x.RetryPolicy != nil {
			if err := convertTypedConfigsToV2(x.RetryPolicy); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *envoy_route_v2.RetryPolicy:
		return nil
	case *envoy_http_v2.HttpFilter:
		if x.ConfigType != nil {
			tc, ok := x.ConfigType.(*envoy_http_v2.HttpFilter_TypedConfig)
			if !ok {
				return fmt.Errorf("%T: ConfigType type %T not handled", x, x.ConfigType)
			}
			if tc.TypedConfig != nil {
				if err := convertTypedConfigsToV2(tc.TypedConfig); err != nil {
					return fmt.Errorf("%T: %w", x, err)
				}
			}
		}
		return nil
	case *envoy_core_v2.ConfigSource:
		if x.ConfigSourceSpecifier != nil {
			if _, ok := x.ConfigSourceSpecifier.(*envoy_core_v2.ConfigSource_Ads); !ok {
				return fmt.Errorf("%T: ConfigSourceSpecifier type %T not handled", x, x.ConfigSourceSpecifier)
			}
		}
		x.ResourceApiVersion = envoy_core_v2.ApiVersion_V2
		return nil
	case *envoy_http_v2.HttpConnectionManager: // "envoy.filters.network.http_connection_manager"
		if x.RouteSpecifier != nil {
			switch spec := x.RouteSpecifier.(type) {
			case *envoy_http_v2.HttpConnectionManager_Rds:
				if spec.Rds != nil && spec.Rds.ConfigSource != nil {
					if err := convertTypedConfigsToV2(spec.Rds.ConfigSource); err != nil {
						return fmt.Errorf("%T: %w", x, err)
					}
				}
			case *envoy_http_v2.HttpConnectionManager_RouteConfig:
				if spec.RouteConfig != nil {
					if err := convertTypedConfigsToV2(spec.RouteConfig); err != nil {
						return fmt.Errorf("%T: %w", x, err)
					}
				}
			default:
				return fmt.Errorf("%T: RouteSpecifier type %T not handled", x, spec)
			}
		}
		for _, filter := range x.HttpFilters {
			if err := convertTypedConfigsToV2(filter); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		if x.Tracing != nil && x.Tracing.Provider != nil && x.Tracing.Provider.ConfigType != nil {
			tc, ok := x.Tracing.Provider.ConfigType.(*envoy_trace_v2.Tracing_Http_TypedConfig)
			if !ok {
				return fmt.Errorf("%T: Tracing.Provider.ConfigType type %T not handled", x, x.Tracing.Provider.ConfigType)
			}
			if err := convertTypedConfigsToV2(tc.TypedConfig); err != nil {
				return fmt.Errorf("%T: %w", x, err)
			}
		}
		return nil
	case *envoy_tls_inspector_v2.TlsInspector: // "envoy.filters.listener.tls_inspector"
		return nil
	case *envoy_tcp_proxy_v2.TcpProxy: // "envoy.filters.network.tcp_proxy"
		return nil
	case *envoy_network_rbac_v2.RBAC: // "envoy.filters.network.rbac"
		return nil
	case *envoy_sni_v2.SniCluster: // "envoy.filters.network.sni_cluster"
		return nil
	case *envoy_http_rbac_v2.RBAC:
		return nil
	case *envoy_tls_v2.UpstreamTlsContext:
		return nil
	case *envoy_tls_v2.DownstreamTlsContext:
		return nil
	case *envoy_grpc_stats_v2.FilterConfig:
		return nil
	default:
		return fmt.Errorf("could not convert unexpected type to v2: %T", pb)
	}
}

func convertTypeUrlsToV2(typeUrl *string) error {
	if _, ok := typeConvert2to3[*typeUrl]; ok {
		return nil // already happened
	}

	converted, ok := typeConvert3to2[*typeUrl]
	if !ok {
		return fmt.Errorf("could not convert type url to v2: %s", *typeUrl)
	}
	*typeUrl = converted
	return nil
}

func convertTypeUrlsToV3(typeUrl *string) error {
	if _, ok := typeConvert3to2[*typeUrl]; ok {
		return nil // already happened
	}

	converted, ok := typeConvert2to3[*typeUrl]
	if !ok {
		return fmt.Errorf("could not convert type url to v3: %s", *typeUrl)
	}
	*typeUrl = converted
	return nil
}

var (
	typeConvert2to3 map[string]string
	typeConvert3to2 map[string]string
)

func init() {
	typeConvert2to3 = make(map[string]string)
	typeConvert3to2 = make(map[string]string)

	reg := func(type2, type3 string) {
		if type2 == "" {
			panic("v2 type is empty")
		}
		if type3 == "" {
			panic("v3 type is empty")
		}
		typeConvert2to3[type2] = type3
		typeConvert3to2[type3] = type2
	}
	reg2 := func(pb2, pb3 proto.Message) {
		any2, err := ptypes.MarshalAny(pb2)
		if err != nil {
			panic(err)
		}
		any3, err := ptypes.MarshalAny(pb3)
		if err != nil {
			panic(err)
		}

		reg(any2.TypeUrl, any3.TypeUrl)
	}

	// primary resources
	reg2(&envoy_api_v2.Listener{}, &envoy_listener_v3.Listener{})                           // LDS
	reg2(&envoy_api_v2.Cluster{}, &envoy_cluster_v3.Cluster{})                              // CDS
	reg2(&envoy_api_v2.RouteConfiguration{}, &envoy_route_v3.RouteConfiguration{})          // RDS
	reg2(&envoy_api_v2.ClusterLoadAssignment{}, &envoy_endpoint_v3.ClusterLoadAssignment{}) // EDS

	// filters
	reg2(&envoy_http_v2.HttpConnectionManager{}, &envoy_http_v3.HttpConnectionManager{}) // "envoy.filters.network.http_connection_manager"
	reg2(&envoy_tcp_proxy_v2.TcpProxy{}, &envoy_tcp_proxy_v3.TcpProxy{})                 // "envoy.filters.network.tcp_proxy"
	reg2(&envoy_network_rbac_v2.RBAC{}, &envoy_network_rbac_v3.RBAC{})                   // "envoy.filters.network.rbac"
	reg2(&envoy_http_rbac_v2.RBAC{}, &envoy_http_rbac_v3.RBAC{})                         // "envoy.filters.http.rbac
	reg2(&envoy_tls_inspector_v2.TlsInspector{}, &envoy_tls_inspector_v3.TlsInspector{}) // "envoy.filters.listener.tls_inspector"
	reg2(&envoy_sni_v2.SniCluster{}, &envoy_sni_v3.SniCluster{})                         // "envoy.filters.network.sni_cluster"

	// cluster tls
	reg2(&envoy_tls_v2.UpstreamTlsContext{}, &envoy_tls_v3.UpstreamTlsContext{})
	reg2(&envoy_tls_v2.DownstreamTlsContext{}, &envoy_tls_v3.DownstreamTlsContext{})

	// extension elements
	reg2(&envoy_metrics_v2.DogStatsdSink{}, &envoy_metrics_v3.DogStatsdSink{})
	reg2(&envoy_metrics_v2.StatsdSink{}, &envoy_metrics_v3.StatsdSink{})
	reg2(&envoy_trace_v2.ZipkinConfig{}, &envoy_trace_v3.ZipkinConfig{})
	reg2(&envoy_grpc_stats_v2.FilterConfig{}, &envoy_grpc_stats_v3.FilterConfig{})
}
