package bizerr

import (
	"context"
	"reflect"
	"strings"

	"github.com/tidbcloud/pkgv2/bizerr"
	"google.golang.org/grpc"
)

type ErrCodeSetter func(resp interface{}, err bizerr.BizError) (resp_ interface{}, err_ error)

func UnaryServerInterceptor(errCodeSetter ErrCodeSetter) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		resp, err := handler(ctx, req)
		if err == nil {
			return resp, err
		}

		be, ok := err.(bizerr.BizError)
		if !ok {
			return resp, err
		}
		// val will be invalid if type and value are both nil, val will be nil if value is nil but type is not nil.
		if val := reflect.ValueOf(resp); !val.IsValid() || val.IsNil() {
			tp := getRespType(ctx, info)
			if tp == nil {
				return resp, err
			}
			resp = reflect.New(tp).Interface()
		}

		be = be.Redact()
		return errCodeSetter(resp, be)
	}
}

// getRespType get response type by get return type of method which reflected from unaryServerInfo.Server
// TODO(chenfei): opt, it's hack. more reasonable way is better
func getRespType(ctx context.Context, info *grpc.UnaryServerInfo) reflect.Type {
	fullMethodName, ok := grpc.Method(ctx)
	if !ok {
		return nil
	}
	pos := strings.LastIndex(fullMethodName, "/")
	if pos < 0 {
		return nil
	}
	methodName := fullMethodName[pos+1:]
	method := reflect.ValueOf(info.Server).MethodByName(methodName)
	if !method.IsValid() {
		return nil
	}
	// two values have been returned, the first is the return message of method which we need, the second is the error.
	tp := method.Type().Out(0).Elem()
	return tp
}
