package auth

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

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/dataflow-service-ng/app/container"
	"github.com/tidbcloud/dataflow-service-ng/app/utils"
	"github.com/tidbcloud/dataflow-service-ng/internal/cmd/bootstrap/server/wrapper"
	"github.com/tidbcloud/dataflow-service-ng/internal/config"
	"github.com/tidbcloud/dataflow-service-ng/pkg/account/accountdb"
	"github.com/tidbcloud/dataflow-service-ng/pkg/log"
	accountAuth "github.com/tidbcloud/tiinf/sdk/auth"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
)

const (
	// orgIdPrjIdPrefix is used to prepend to the paths in openAPIURLResourceMap so that the org_id and project_id can be extracted from the path.
	orgIdPrjIdPrefix = `/orgs/(\d+)/projects/(\d+)`
	// orgIdPrjIdTidbIdPrefix also captures the middleware-resolved TiDB ID for public paths that omit it.
	orgIdPrjIdTidbIdPrefix = `/orgs/(\d+)/projects/(\d+)/tidbs/(\d+)`
)

// all resources are under the tidbs path
var urlResourceMap = map[string]string{
	"get:/api/v1beta2/tidbs/(\\d+)/changefeeds$":               "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/list",
	"get:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+)":         "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/get",
	"get:/api/v1beta2/tidbs/(\\d+)/tcu":                        "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/tcu",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+):resume": "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/resume",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+):pause":  "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/pause",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds$":              "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/create",
	"delete:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+)":      "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/delete",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds/test":          "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/test",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+):edit":   "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/edit",
	"get:/api/v1beta2/tidbs/(\\d+)/enabledDPFeatures":          "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/enabledDPFeatures",
	"post:/api/v1beta2/tidbs/(\\d+)/describeSchemaTable":       "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/describeSchemaTable",
	"post:/api/v1beta2/tidbs/(\\d+)/changefeeds/(\\d+):scale":  "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/scale",
	"post:/api/v1beta2/tidbs/(\\d+)/calChangefeedFee":          "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/calChangefeedFee",
	"get:/api/v1beta2/tidbs/(\\d+)/adminProfile":               "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/adminProfile",
	"get:/api/v1beta2/tidbs/(\\d+)/changefeedSpecifications":   "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeedSpecifications",

	// Data Pipeline carries its own privileges rather than borrowing the
	// changefeed's. A pipeline is not a changefeed: it owns an export and a
	// destination integration as well, so granting someone changefeed rights
	// would hand them those too, and revoking changefeed rights would take a
	// pipeline away from someone who was never given one.
	//
	// Two privileges, split by whether the call can change anything, as
	// premium migration does. Anything finer would be a matrix nobody
	// maintains: the operations here are only meaningful in sequence, and a
	// role allowed to create a pipeline but not resume it can do nothing
	// useful with either.
	"get:/api/v1beta2/tidbs/(\\d+)/data-pipelines.*":    "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/data_pipeline/view",
	"post:/api/v1beta2/tidbs/(\\d+)/data-pipelines.*":   "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/data_pipeline/modify",
	"delete:/api/v1beta2/tidbs/(\\d+)/data-pipelines.*": "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/data_pipeline/modify",

	"post:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints$":                                 "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/create",
	"get:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints$":                                  "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/list",
	"get:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/(\\d+)":                            "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/get",
	"delete:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/(\\d+)":                         "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/delete",
	"get:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/availabilityZones":                 "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/availabilityZones",
	"get:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/endpointServiceAvailabilityZones":  "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/availabilityZones",
	"post:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/generateAdvertisedListener":       "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/generateAdvertisedListener",
	"post:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/validateAdvertisedListenerDomain": "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/validateAdvertisedListenerDomain",
	"get:/api/v1beta2/tidbs/(\\d+)/sinkEndpoints/authInfo":                          "/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/sinkEndpoints/authInfo",
}

// innerOpenAPIURLResourceMap is for predefined only, don't directly use it, use openAPIURLResourceMap or resourceMap instead.
var innerOpenAPIURLResourceMap = map[string]string{
	`get:/v1beta2/tidbs/(\d+)/imports.*`:  `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/import_into/view`,
	`post:/v1beta2/tidbs/(\d+)/imports.*`: `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/import_into/modify`,

	`get:/v1beta2/tidbs/(\d+)/premiumMigration.*`:    `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/premium_migration/view`,
	`post:/v1beta2/tidbs/(\d+)/premiumMigration.*`:   `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/premium_migration/modify`,
	`put:/v1beta2/tidbs/(\d+)/premiumMigration.*`:    `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/premium_migration/modify`,
	`delete:/v1beta2/tidbs/(\d+)/premiumMigration.*`: `nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/premium_migration/modify`,
}

// innerOpenAPIWithoutTidbIDURLResourceMap contains routes whose public paths
// omit tidb_id. They are normalized to TiDB-scoped paths before registration.
var innerOpenAPIWithoutTidbIDURLResourceMap = map[string]string{
	`get:/v1beta2/changefeeds$`:                           `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/list`,
	`post:/v1beta2/changefeeds$`:                          `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/create`,
	`get:/v1beta2/changefeeds/\d+$`:                       `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/get`,
	`delete:/v1beta2/changefeeds/\d+$`:                    `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/delete`,
	`post:/v1beta2/changefeeds/\d+:pause$`:                `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/pause`,
	`post:/v1beta2/changefeeds/\d+:resume$`:               `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeeds/resume`,
	`post:/v1beta2/changefeeds/\d+:editDownstreamConfig$`: `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/edit`,
	`post:/v1beta2/changefeeds/\d+:scale$`:                `/nextgen/orgs/{org_id}/projects/{project_id}/tidbs/{cluster_id}/changefeed/scale`,
}

var openAPIURLResourceMap = make(map[string]string,
	len(innerOpenAPIURLResourceMap)+len(innerOpenAPIWithoutTidbIDURLResourceMap))
var resourceMap = make(map[*regexp.Regexp]string)

func registerOpenAPIURLResources(routeMap map[string]string, pathPrefix string, mergeIntoConsoleRoutes bool) {
	for k, v := range routeMap {
		index := strings.Index(k, accountAuth.PathMethodSeparator)
		if index < 0 {
			panic(fmt.Sprintf("invalid path_pattern: %s", k))
		}
		methodStr := k[0:index]
		pathStr := k[index+1:]

		if mergeIntoConsoleRoutes {
			urlResourceMap[k] = v
		}
		prependedKey := methodStr + accountAuth.PathMethodSeparator + pathPrefix + pathStr
		openAPIURLResourceMap[prependedKey] = v
	}
}

func init() {
	registerOpenAPIURLResources(innerOpenAPIURLResourceMap, orgIdPrjIdPrefix, true)
	registerOpenAPIURLResources(innerOpenAPIWithoutTidbIDURLResourceMap, orgIdPrjIdTidbIdPrefix, false)

	for k, v := range urlResourceMap {
		k = strings.ToLower(k)
		resourceMap[regexp.MustCompile(k)] = v
	}
}

var rbacAuthSingleton = &rbacAuth{}

func RBAC(ctx *gin.Context) {
	authType := ctx.GetHeader(APIAuthTypeKey)
	if authType == string(APIAuthTypeOpenAPI) {
		logger := log.GetGinLogger(ctx)
		logger.Info("`RBAC` middleware is only for console API")
		wrapper.NewAPIError(ctx, http.StatusUnauthorized, UnauthenticatedError)
		return
	}
	rbacAuthSingleton.Auth(ctx)
}

type rbacAuth struct{}

func getRBACResource(path string, method string) string {
	method = strings.ToLower(method)
	path = strings.ToLower(path)
	for p, uri := range resourceMap {
		if p.MatchString(strings.ToLower(fmt.Sprintf("%s:%s", method, path))) {
			return uri
		}
	}
	return ""
}

func getClusterIDParam(ctx *gin.Context) string {
	clusterID := ctx.Param(ClusterID)
	if clusterID != "" {
		return clusterID
	}
	return ctx.Param("tidb_id")
}

func isRBACBypassUser(email string, bypassUserEmails []string) bool {
	email = strings.ToLower(strings.TrimSpace(email))
	if email == "" {
		return false
	}

	for _, bypassUserEmail := range bypassUserEmails {
		if email == strings.ToLower(strings.TrimSpace(bypassUserEmail)) {
			return true
		}
	}
	return false
}

func (r *rbacAuth) Auth(ctx *gin.Context) {
	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)
	if isRBACBypassUser(user.Email, config.Global().ServerConfig.RBACBypassUserEmails) {
		log.GetGinLogger(ctx).Info("rbac bypassed for user", zap.String("path", ctx.Request.URL.Path))
		return
	}

	rbacResource := 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
	}

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

	resourceParam, err := GenResourceParam4GinCtx(ctx)
	if err != nil {
		log.GetGinLogger(ctx).Warn("failed to generate resource param", zap.String("path", ctx.Request.URL.Path), zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusForbidden, err)
		return
	}
	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("project_id", 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("project_id", 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 GenResourceParam4GinCtx(ctx *gin.Context) (map[string]string, error) {
	resourceParam := map[string]string{}

	clusterIdStr := getClusterIDParam(ctx)
	if clusterIdStr != "" {
		clusterID, err := strconv.ParseUint(clusterIdStr, 10, 64)
		if err != nil {
			log.GetGinLogger(ctx).Warn("failed to parse cluster id",
				zap.String("cluster_id", clusterIdStr))
			return nil, ResourceNotFoundError
		}
		cluster, err := container.Global().MgmtSvcGrpcClient.GetClusterByID(ctx.Request.Context(), clusterID)
		if err != nil {
			log.GetGinLogger(ctx).Warn("failed to get cluster",
				zap.Uint64(ClusterID, clusterID))
			return nil, ResourceNotFoundError
		}
		clusterWrapper := utils.NewCluster(cluster)
		resourceParam[RBACClusterIdKey] = clusterIdStr
		resourceParam[RBACOrgIdKey] = strconv.FormatUint(clusterWrapper.GetTenantID(), 10)
		resourceParam[RBACProjectIdKey] = strconv.FormatUint(clusterWrapper.GetProjectID(), 10)
	} else {
		if orgID := ctx.Param(OrgID); orgID != "" {
			resourceParam[RBACOrgIdKey] = orgID
		}
	}
	return resourceParam, nil
}
