package grpc

import (
	"context"
	"errors"
	"reflect"
	"strings"

	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

// The OldCoreBizErrToBaseRespInterceptor converts failed responses (non-200) into successful ones (200) by embedding the error code and message within baseResp.
// This approach, while not ideal for managing business errors between the frontend and backend,
// serves as a temporary workaround to maintain compatibility.
func OldCoreBizErrToBaseRespInterceptor() grpc.UnaryServerInterceptor {
	return BuildBizErrTransformInterceptor(func(resp any, err bizerr.BizError) (any, error) {
		baseResp := getBaseRespFromResp(resp)
		if baseResp == nil {
			return resp, err
		}

		// If baseResp field is present, it means the gRPC is migrated from core-svc.
		// So we should set the error code and message into the baseResp for compatibility.
		baseResp.ErrCode = int64(err.ErrCode())
		baseResp.ErrMsg = err.ErrMsg()

		return resp, err
	})
}

type BizErrMapFunc func(resp any, err bizerr.BizError) (resp_ any, err_ error)

// BuildBizErrTransformInterceptor constructs an interceptor that ensures the arguments passed to bizErrMapFunc
// are concrete objects of the response type and bizerr.BizError.
// If the original error is not of type bizerr.BizError, it will be returned directly without calling bizErrMapFunc.
func BuildBizErrTransformInterceptor(bizErrMapFunc BizErrMapFunc) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		resp, err := handler(ctx, req)
		if err == nil {
			return resp, nil
		}

		var be bizerr.BizError

		if ok := errors.As(err, &be); !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()
		}

		return bizErrMapFunc(resp, be)
	}
}

// In the behavior of the old core-svc and core-portal, when a gRPC method returns an error:
//   - Only errors with `bizerr.ErrCodeUnknown` or an undefined `bizerr.Code` are recognized as failed responses (non-200).
//   - All other error codes are interpreted as successful responses (200).
//   - The corresponding error code and message are encapsulated within baseResp.
func OldCoreClearErrorInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		resp, err := handler(ctx, req)
		if err == nil {
			return resp, nil
		}

		grpcStatusErrWithoutDetails := func() (any, error) {
			// If the error is a gRPC status error.
			if s, ok := status.FromError(err); ok {
				// Redact the Details.
				return resp, status.New(s.Code(), s.Message()).Err()
			}
			return resp, err
		}

		// If resp is nil and err is not nil, return the original error.
		if resp == nil {
			return grpcStatusErrWithoutDetails()
		}

		// Try to get baseResp from resp.
		baseResp := getBaseRespFromResp(resp)
		if baseResp == nil {
			// If baseResp does not exist, return the original error.
			return grpcStatusErrWithoutDetails()
		}
		if bizerr.IsInternalErr(bizerr.ErrCode(baseResp.GetErrCode())) ||
			bizerr.IsException(bizerr.ErrCode(baseResp.GetErrCode())) {
			// If baseResp exists and the error code is internal error or exception, return the original error.
			return grpcStatusErrWithoutDetails()
		}

		// Finally, if baseResp exists and the error code is not business error, we should remove error completely.
		return resp, nil
	}
}

func getBaseRespFromResp(resp any) *base.BaseResp {
	if resp == nil {
		return nil
	}

	val := reflect.ValueOf(resp)
	if !val.IsValid() || val.Kind() != reflect.Ptr || val.IsNil() {
		return nil
	}

	if !reflect.ValueOf(resp).Elem().IsValid() {
		return nil
	}

	baseResp := reflect.ValueOf(resp).Elem().FieldByName("BaseResp")
	if !baseResp.IsValid() { // don't have BaseReq field
		return nil
	}
	if baseResp.IsNil() {
		baseResp.Set(reflect.New(baseResp.Type().Elem()))
	}

	typed, ok := baseResp.Interface().(*base.BaseResp)
	if !ok {
		return nil
	}
	return typed
}

// getRespType get response type by get return type of method which reflected from unaryServerInfo.Server
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
}

// CoreClientBaseRespToGrpcStatusErrorInterceptor converts the baseResp in the response to a gRPC status error.
// This is the **reversed** version of OldCoreBizErrToBaseRespInterceptor.
// Clients of server using OldCoreBizErrToBaseRespInterceptor could use this interceptor to convert the baseResp back to gRPC status error.
func CoreClientBaseRespToGrpcStatusErrorInterceptor(
	ctx context.Context,
	method string,
	req, reply any,
	cc *grpc.ClientConn,
	invoker grpc.UnaryInvoker,
	opts ...grpc.CallOption,
) error {
	err := invoker(ctx, method, req, reply, cc, opts...)

	// Try to get baseResp from reply.
	baseResp := getBaseRespFromResp(reply)
	if baseResp == nil {
		return err
	}

	// If baseResp.GetErrCode() is 0, it means the response is successful.
	if baseResp.GetErrCode() == 0 {
		return err
	}

	return status.New(codes.Code(baseResp.GetErrCode()), baseResp.GetErrMsg()).Err()
}
