package loadroutes

import (
	"fmt"
	"reflect"

	"code.justin.tv/common/alvin/internal/httpproto/google_api"
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/protoc-gen-go/descriptor"
)

// getHttpRule retrieves any google.api.http extension value attached to the
// provided rpc method.
//
// Critically, it avoids using the proto.GetExtension function which is backed
// by a global map of extensions which are registered on package
// initialization. Once an extension is registered, subsequent registrations
// of similar extensions will panic. This means that all packages linked into
// our users' programs would need to agree on a single Go package to host the
// google.api.http extensions. One top contender is grpc-gateway.
//
// This leads us to a choice between three alternatives:
//
//   1. Depend on grpc-gateway. That is a large dependency to force on our
//   users.
//
//   2. Depend on a package other than grpc-gateway to include the extension,
//   possibly one under our control. That option prevents using Alvin with
//   grpc-gateway (or any packages that use it) in the same program.
//
//   3. Load the extension in secret, using low-level protobuf APIs.
//
// This function implements the third option.
func getHttpRule(opts *descriptor.MethodOptions) (*google_api.HttpRule, error) {
	ext, err := getUnregisteredExtension(opts, eHttp)
	if err != nil {
		return nil, err
	}
	// getUnregisteredExtension's use of convertProtoType guarantees that if
	// it returns a nil error, the extension value will be non-nil and will
	// have the correct type.
	return ext.(*google_api.HttpRule), nil
}

// eHttp is a private copy of the google.api.http extension description. This
// allows the Alvin packages to avoid registering extensions in the proto
// package's global map, which would cause a panic on init due to double-
// registration if used in conjunction with code like grpc-gateway.
var eHttp = &proto.ExtensionDesc{
	ExtendedType:  (*descriptor.MethodOptions)(nil),
	ExtensionType: (*google_api.HttpRule)(nil),
	// Field number from google/api/annotations.proto, found at
	// https://github.com/googleapis/googleapis/blob/master/google/api/annotations.proto
	Field: 72295728,
	Tag:   "bytes,72295728,opt,name=http",

	// Name and Filename fields are not required for operation
}

// getUnregisteredExtension parses and returns the given extension of pb. If
// the extension is not present and has no default value it returns
// ErrMissingExtension.
//
// This function is similar to proto.GetExtension, but works on extensions
// that have not been entered into the global registry maintained by the proto
// package. This is helpful for some Google-provided extensions (such as the
// google.api.http extension to google.protobuf.MethodOptions) which have
// well-known definitions but do not have a single well-known Go package.
func getUnregisteredExtension(pb proto.Message, extension *proto.ExtensionDesc) (interface{}, error) {
	descs, err := proto.ExtensionDescs(pb)
	if err != nil {
		return nil, err
	}
	var found *proto.ExtensionDesc
	for _, desc := range descs {
		if desc.Field == extension.Field {
			found = desc
			break
		}
	}
	if found == nil {
		// extension not present
		return nil, proto.ErrMissingExtension
	}
	if found.ExtendedType == nil {
		found.ExtendedType = extension.ExtendedType
	}
	if found.ExtensionType == nil {
		found.ExtensionType = extension.ExtensionType
	}
	if found.Tag == "" {
		found.Tag = extension.Tag
	}

	ext, err := proto.GetExtension(pb, found)
	if err != nil {
		return nil, err
	}

	// Convert to the type requested by the caller. The type returned by
	// proto.GetExtension will be different from what our caller requested if
	// a similar but distinct extension has been registered in the proto
	// package's global map. This can happen when using popular protobuf types
	// that do not yet have a popular Go import path.
	return convertProtoType(ext, reflect.TypeOf(extension.ExtensionType))
}

// convertProtoType converts a protobuf message of one type into a protobuf
// message of a different type with compatible layout. This is useful when
// using extensions that may have been registered with a different (but
// compatible) type.
func convertProtoType(msg interface{}, outType reflect.Type) (interface{}, error) {
	inPb, ok := msg.(proto.Message)
	if !ok {
		return nil, fmt.Errorf("non-protobuf input type %T", msg)
	}

	buf, err := proto.Marshal(inPb)
	if err != nil {
		return nil, err
	}

	if outType.Kind() == reflect.Ptr {
		outType = outType.Elem()
	}
	out := reflect.New(outType).Interface()
	outPb, ok := out.(proto.Message)
	if !ok {
		return nil, fmt.Errorf("non-protobuf output type %T", out)
	}

	err = proto.Unmarshal(buf, outPb)
	if err != nil {
		return nil, err
	}

	return out, nil
}
