package auth

import (
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/dataflow-service/internal/cmd/bootstrap/server/wrapper"
	"github.com/tidbcloud/dataflow-service/internal/config"
	"github.com/tidbcloud/dataflow-service/internal/grpc/grpcli"
	"github.com/tidbcloud/dataflow-service/pkg/account/accountdb"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/dataflow-service/pkg/parse"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
)

var rbacAuthSingleton = &rbacAuth{}

func RBAC(ctx *gin.Context) {
	authType := ctx.GetHeader(APIAuthTypeKey)
	if authType == string(APIAuthTypeOpenAPI) {
		OpenAPIRBAC(ctx)
		return
	}
	rbacAuthSingleton.Auth(ctx)
}

type rbacAuth struct{}

func (c *rbacAuth) Auth(ctx *gin.Context) {
	if !config.Global().RBACConfig.Switch {
		return
	}

	if config.Global().RBACConfig.IsIgnoreRBACPath(ctx.Request.URL.Path) {
		return
	}

	rbacResource := config.Global().RBACConfig.GetRBACResource(ctx.Request.URL.Path, ctx.Request.Method)
	if rbacResource == "" {
		log.GetGinLogger(ctx).Warn("failed to get rbac resource", zap.String("path", ctx.Request.URL.Path))
		wrapper.NewAPIError(ctx, http.StatusForbidden, ResourceNotFoundError)
		return
	}

	value, exists := ctx.Get(DataflowDefineUser)
	if !exists {
		log.GetGinLogger(ctx).Warn("failed to get user")
		wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
		return
	}
	user := value.(*accountdb.User)

	identity := strconv.FormatUint(user.User.ID, 10)

	// hard code to set useless id for rbac check, the ids is not used in rbac check
	resourceParam := map[string]string{
		"id":                "0",
		"connector_id":      "0",
		"changefeed_id":     "0",
		"dw_replication_id": "0",
		"recovery_group_id": "0",
		"backup_id":         "0",
		"export_id":         "0",
	}
	if strings.Contains(rbacResource, RBACOrgIdKey) {
		value, exists := ctx.Get(OrgID)
		if !exists {
			log.GetGinLogger(ctx).Warn("failed to get org id")
			wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
			return
		}
		resourceParam[RBACOrgIdKey] = strconv.FormatUint(value.(uint64), 10)
	}
	if strings.Contains(rbacResource, RBACProjectIdKey) {
		value, exists := ctx.Get(ProjectID)
		if !exists {
			log.GetGinLogger(ctx).Warn("failed to get project id")
			wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
			return
		}
		resourceParam[RBACProjectIdKey] = strconv.FormatUint(value.(uint64), 10)
	}
	if strings.Contains(rbacResource, RBACClusterIdKey) {
		value, exists := ctx.Get(ClusterID)
		if !exists {
			log.GetGinLogger(ctx).Warn("failed to get cluster id")
			wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
			return
		}
		resourceParam[RBACClusterIdKey] = strconv.FormatUint(value.(uint64), 10)
	}
	permission, msg, err := useadapter.CheckIdentityPermission(
		ctx.Request.Context(), identity, rbacResource, resourceParam)
	if err != nil {
		log.GetGinLogger(ctx).Warn("failed to check identity permission",
			zap.String("message", msg),
			zap.String(OrgID, resourceParam[RBACOrgIdKey]),
			zap.String(ProjectID, resourceParam[RBACProjectIdKey]),
			zap.String(ClusterID, resourceParam[RBACClusterIdKey]),
			zap.Any("user", user.User),
			zap.String("resource_uri", rbacResource),
			zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusForbidden, UserPermissionDeniedError)
		return
	}

	if !permission {
		log.GetGinLogger(ctx).Warn("no permission",
			zap.String("message", msg),
			zap.String(OrgID, resourceParam[RBACOrgIdKey]),
			zap.String(ProjectID, resourceParam[RBACProjectIdKey]),
			zap.String(ClusterID, resourceParam[RBACClusterIdKey]),
			zap.Any("user", user.User),
			zap.String("resource_uri", rbacResource),
			zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusForbidden, UserPermissionDeniedError)
		return
	}

	log.GetGinLogger(ctx).Info("rbac successful", zap.String("path", ctx.Request.URL.Path))
}

func OpenAPIRBAC(ctx *gin.Context) {
	path := ctx.Request.URL.Path
	method := ctx.Request.Method

	// only check rbac for v1beta1 apis
	if !strings.HasPrefix(path, "/v1beta1/") ||
		!config.Global().RBACConfig.Switch ||
		config.Global().RBACConfig.IsIgnoreRBACPath(ctx.Request.URL.Path) {
		return
	}
	rbacResource := config.Global().RBACConfig.GetRBACResource(path, method)
	if rbacResource == "" {
		log.GetGinLogger(ctx).Warn("failed to get rbac resource",
			zap.String("method", method),
			zap.String("path", path))
		return
	}

	value, exists := ctx.Get(DataflowDefineUser)
	if !exists {
		log.GetGinLogger(ctx).Warn("failed to get user")
		wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
		return
	}
	user := value.(*accountdb.User)

	orgId := user.TenantID

	projectId := uint64(0)
	clusterId := uint64(0)

	// get cluster_id from path and get project_id from cluster_id
	if parsedClusterID, ok := parse.ParseClusterIDFromPath(path); ok {
		cluster, err := grpcli.GetClusterByID(ctx.Request.Context(), parsedClusterID)
		if err != nil {
			log.GetGinLogger(ctx).Warn("failed to get cluster by id",
				zap.Uint64("cluster_id", parsedClusterID),
				zap.Error(err))
			wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
			return
		}

		projectId = cluster.ProjectId
		clusterId = cluster.Id
	}
	resourceParam := map[string]string{
		RBACOrgIdKey:     strconv.FormatUint(orgId, 10),
		RBACProjectIdKey: strconv.FormatUint(projectId, 10),
		RBACClusterIdKey: strconv.FormatUint(clusterId, 10),

		// hard code to set useless id for rbac check, the ids is not used in rbac check
		"backup_id": "0",
		"export_id": "0",
		"import_id": "0",
	}
	permission, err := auth.CheckIdentityPermission(ctx, &auth.CheckPermissionParam{
		IdentityTye:   auth.AccessIdentityTypeApiKeyAK,
		IdentityVal:   user.LastName,
		ResourceUri:   rbacResource,
		ResourceParam: resourceParam,
	})
	if err != nil {
		log.GetGinLogger(ctx).Warn("failed to check identity permission",
			zap.String(OrgID, resourceParam[RBACOrgIdKey]),
			zap.String(ProjectID, resourceParam[RBACProjectIdKey]),
			zap.String(ClusterID, resourceParam[RBACClusterIdKey]),
			zap.Any("user", user.User),
			zap.String("resource_uri", rbacResource),
			zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusForbidden, UserPermissionDeniedError)
		return
	}
	if !permission.HasPermission {
		log.GetGinLogger(ctx).Warn("no permission",
			zap.String("message", permission.Message),
			zap.String(OrgID, resourceParam[RBACOrgIdKey]),
			zap.String(ProjectID, resourceParam[RBACProjectIdKey]),
			zap.String(ClusterID, resourceParam[RBACClusterIdKey]),
			zap.Any("user", user.User),
			zap.String("resource_uri", rbacResource),
			zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusForbidden, UserPermissionDeniedError)
		return
	}
	log.GetGinLogger(ctx).Info("rbac successful", zap.String("path", ctx.Request.URL.Path), zap.String("resource_uri", rbacResource))
}
