/*
 *
 * Copyright 2021 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// Package googledirectpath implements a resolver that configures xds to make
// cloud to prod directpath connection.
//
// It's a combo of DNS and xDS resolvers. It delegates to DNS if
// - not on GCE, or
// - xDS bootstrap env var is set (so this client needs to do normal xDS, not
// direct path, and clients with this scheme is not part of the xDS mesh).
package googledirectpath

import (
	"fmt"
	"net/url"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/google"
	"google.golang.org/grpc/grpclog"
	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/googlecloud"
	internalgrpclog "google.golang.org/grpc/internal/grpclog"
	"google.golang.org/grpc/internal/grpcrand"
	"google.golang.org/grpc/resolver"
	_ "google.golang.org/grpc/xds" // To register xds resolvers and balancers.
	"google.golang.org/grpc/xds/internal/xdsclient"
	"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
	"google.golang.org/protobuf/types/known/structpb"

	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
)

const (
	c2pScheme             = "google-c2p"
	c2pExperimentalScheme = "google-c2p-experimental"
	c2pAuthority          = "traffic-director-c2p.xds.googleapis.com"

	tdURL          = "dns:///directpath-pa.googleapis.com"
	httpReqTimeout = 10 * time.Second
	zoneURL        = "http://metadata.google.internal/computeMetadata/v1/instance/zone"
	ipv6URL        = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"

	gRPCUserAgentName               = "gRPC Go"
	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
	ipv6CapableMetadataName         = "TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE"

	logPrefix = "[google-c2p-resolver]"

	dnsName, xdsName = "dns", "xds"
)

// For overriding in unittests.
var (
	onGCE = googlecloud.OnGCE

	newClientWithConfig = func(config *bootstrap.Config) (xdsclient.XDSClient, func(), error) {
		return xdsclient.NewWithConfig(config)
	}

	logger = internalgrpclog.NewPrefixLogger(grpclog.Component("directpath"), logPrefix)
)

func init() {
	resolver.Register(c2pResolverBuilder{
		scheme: c2pScheme,
	})
	// TODO(apolcyn): remove this experimental scheme before the 1.52 release
	resolver.Register(c2pResolverBuilder{
		scheme: c2pExperimentalScheme,
	})
}

type c2pResolverBuilder struct {
	scheme string
}

func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	if t.URL.Host != "" {
		return nil, fmt.Errorf("google-c2p URI scheme does not support authorities")
	}

	if !runDirectPath() {
		// If not xDS, fallback to DNS.
		t.Scheme = dnsName
		t.URL.Scheme = dnsName
		return resolver.Get(dnsName).Build(t, cc, opts)
	}

	// Note that the following calls to getZone() and getIPv6Capable() does I/O,
	// and has 10 seconds timeout each.
	//
	// This should be fine in most of the cases. In certain error cases, this
	// could block Dial() for up to 10 seconds (each blocking call has its own
	// goroutine).
	zoneCh, ipv6CapableCh := make(chan string), make(chan bool)
	go func() { zoneCh <- getZone(httpReqTimeout) }()
	go func() { ipv6CapableCh <- getIPv6Capable(httpReqTimeout) }()

	balancerName := envconfig.C2PResolverTestOnlyTrafficDirectorURI
	if balancerName == "" {
		balancerName = tdURL
	}
	serverConfig := &bootstrap.ServerConfig{
		ServerURI:    balancerName,
		Creds:        grpc.WithCredentialsBundle(google.NewDefaultCredentials()),
		TransportAPI: version.TransportV3,
		NodeProto:    newNode(<-zoneCh, <-ipv6CapableCh),
	}
	config := &bootstrap.Config{
		XDSServer: serverConfig,
		ClientDefaultListenerResourceNameTemplate: "%s",
		Authorities: map[string]*bootstrap.Authority{
			c2pAuthority: {
				XDSServer: serverConfig,
			},
		},
	}

	// Create singleton xds client with this config. The xds client will be
	// used by the xds resolver later.
	_, close, err := newClientWithConfig(config)
	if err != nil {
		return nil, fmt.Errorf("failed to start xDS client: %v", err)
	}

	// Create and return an xDS resolver.
	t.Scheme = xdsName
	t.URL.Scheme = xdsName
	if envconfig.XDSFederation {
		t = resolver.Target{
			URL: url.URL{
				Scheme: xdsName,
				Host:   c2pAuthority,
				Path:   t.URL.Path,
			},
		}
	}
	xdsR, err := resolver.Get(xdsName).Build(t, cc, opts)
	if err != nil {
		close()
		return nil, err
	}
	return &c2pResolver{
		Resolver:        xdsR,
		clientCloseFunc: close,
	}, nil
}

func (b c2pResolverBuilder) Scheme() string {
	return b.scheme
}

type c2pResolver struct {
	resolver.Resolver
	clientCloseFunc func()
}

func (r *c2pResolver) Close() {
	r.Resolver.Close()
	r.clientCloseFunc()
}

var ipv6EnabledMetadata = &structpb.Struct{
	Fields: map[string]*structpb.Value{
		ipv6CapableMetadataName: structpb.NewBoolValue(true),
	},
}

var id = fmt.Sprintf("C2P-%d", grpcrand.Int())

// newNode makes a copy of defaultNode, and populate it's Metadata and
// Locality fields.
func newNode(zone string, ipv6Capable bool) *v3corepb.Node {
	ret := &v3corepb.Node{
		// Not all required fields are set in defaultNote. Metadata will be set
		// if ipv6 is enabled. Locality will be set to the value from metadata.
		Id:                   id,
		UserAgentName:        gRPCUserAgentName,
		UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version},
		ClientFeatures:       []string{clientFeatureNoOverprovisioning},
	}
	ret.Locality = &v3corepb.Locality{Zone: zone}
	if ipv6Capable {
		ret.Metadata = ipv6EnabledMetadata
	}
	return ret
}

// runDirectPath returns whether this resolver should use direct path.
//
// direct path is enabled if this client is running on GCE, and the normal xDS
// is not used (bootstrap env vars are not set) or federation is enabled.
func runDirectPath() bool {
	if !onGCE() {
		return false
	}
	return envconfig.XDSFederation || envconfig.XDSBootstrapFileName == "" && envconfig.XDSBootstrapFileContent == ""
}
