package rbac

import (
	"context"
	"fmt"

	"github.com/pingcap/errcode"
	"github.com/tidbcloud/core-portal/internal/central/config"
	grpc_http "github.com/tidbcloud/core-portal/internal/central/grpc/http"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/tiinf/sdk/rbac"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"
	"github.com/tidbcloud/tiinf/sdk/rbac/helperimpl"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
	"google.golang.org/grpc"
)

var Tracer = otel.Tracer("core-portal")

func GrpcGatewayPermissionCheckUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		ctx, span := Tracer.Start(ctx, "GrpcGatewayPermissionCheckUnaryServerInterceptor")
		defer span.End()
		logger := log.WithContextS(ctx).With(
			zap.String("path", grpc_http.HttpPATH(ctx)), zap.String("method", grpc_http.HttpMethod(ctx)),
		)
		if !config.Global().RBACConfig.Enable {
			return handler(ctx, req)
		}
		// do not inject permission check logic when req is not come from grpc gateway
		if !WhetherRequestComeFromGrpcGateway(ctx) {
			return handler(ctx, req)
		}
		if config.Global().GrpcConfig.RBACServerAddr != "" {
			logger.Infof("use customized permissionHelper: %s", config.Global().GrpcConfig.RBACServerAddr)
			helperimpl.DefaultCachedPermissionHelper.SetEndpoint(config.Global().GrpcConfig.RBACServerAddr)
		}
		checker := rbac.GetPermissionCheckerWithDefaultHelper(
			helperimpl.NewContextStoreGetIdentityFunc(config.CtxIdentityKey),
			config.Global().RBACConfig.MergeRBACPathPattern2ResourceMap,
			config.RBACPathWhitelist...)
		result, err := checker.Handle(ctx, GenAccessParam4GrpcGateway(ctx))
		pass := err == nil && result.Code == common.RSP_CODE_RELEASE
		logger.Infof("RBAC check result, pass=%t, result=%+v,err=%s", pass, result, err)

		if config.Global().RBACConfig.Dryrun {
			ret, handlerErr := handler(ctx, req)

			allowedByRBAC := true
			if err != nil || result.Code != common.RSP_CODE_RELEASE {
				allowedByRBAC = false
			}
			allowedByOldWay := true
			if handlerErr != nil {
				if ec, ok := handlerErr.(errcode.ErrorCode); ok && ec.Code() == errcode.ForbiddenCode {
					allowedByOldWay = false
				}
			}
			if allowedByRBAC != allowedByOldWay {
				//TODO(Sentry)
				logger.Infof("RBAC dryrun, result mismatch:  allowedByRBAC=%t, allowedByOldWay=%t", allowedByRBAC, allowedByOldWay)
			} else {
				logger.Infof("RBAC dryrun, pass")
			}
			return ret, handlerErr
		}

		if err != nil {
			return nil, errcode.NewForbiddenErr(fmt.Errorf("permission denied, err=%s", err))
		}
		if result.Code != common.RSP_CODE_RELEASE {
			return nil, errcode.NewForbiddenErr(fmt.Errorf("permission denied, code=%d", result.Code))
		}
		return handler(ctx, req)
	}
}

func GenAccessParam4GrpcGateway(ctx context.Context) *common.AccessParam {
	param := common.AccessParam{
		Metadata: map[string]interface{}{
			common.METADATA_PATH:   grpc_http.HttpPATH(ctx),
			common.METADATA_METHOD: grpc_http.HttpMethod(ctx),
		},
		Req: ctx,
	}
	return &param
}

// WhetherRequestComeFromGrpcGateway return true if the request come from "/api/v1/*", return false if the request
// is initialized via call gRPC directly or come from "/api/v1beta/*"
func WhetherRequestComeFromGrpcGateway(ctx context.Context) bool {
	token := GetGrpcGatewayFromContext(ctx)
	return token == config.CtxGRPCGatewayTokenValue
}

func GetGrpcGatewayFromContext(ctx context.Context) string {
	iface := ctx.Value(config.CtxGRPCGatewayTokenKey)
	if iface == nil {
		return ""
	}
	token, ok := iface.(string)
	if !ok {
		return ""
	}
	return token
}
