package rbac

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/pingcap/errcode"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/grpc/grpcli"
	grpc_http "github.com/tidbcloud/serverless-service/internal/grpc/http"
	"github.com/tidbcloud/serverless-service/internal/utils"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
	"google.golang.org/grpc"
)

var (
	permissionChecker                auth.PermissionChecker
	errorForbidden                   = errcode.NewForbiddenErr(errors.New("permission denied"))
	recycledInstancesListPathPattern = regexp.MustCompile(`^/api/v1/serverless/orgs/\d+/recycledInstances/?$`)
)

func init() {
	auth.InitAccountClientByEndpoint("account-provider-grpc:10000", 0)
	var err error
	permissionChecker, err = auth.NewPermissionCheckerWithCommonParam(&auth.PermissionCheckerCommonParam{
		IdentityF:               GetConsoleIdentity,
		PathPattern2ResourceMap: DefaultRBACPathPattern2ResourceMap,
		PathWhitelist:           RBACPathWhitelist,
	})
	if err != nil {
		panic(err)
	}
}

func GetConsoleIdentity(ctx context.Context, param *auth.AccessParam) (*auth.IdentityRet, error) {
	identityValue := ctx.Value(config.CtxIdentityValKey)
	if identityValue == nil {
		return nil, errors.New("not found identity value in context")
	}
	identityType := ctx.Value(config.CtxIdentityTypeKey)
	if identityType == nil {
		return nil, errors.New("not found identity type info in context")
	}
	if identityType != auth.AuthnIdentityTypeUserToken {
		return nil, errors.New("not user id type identity")
	}
	ret := auth.IdentityRet{
		IdentityType:  auth.AccessIdentityTypeUserID,
		IdentityValue: identityValue.(string),
	}
	return &ret, nil
}

// UnaryServerInterceptor returns a new unary server interceptor that performs rbac check
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		if !config.Global().ServerConfig.RBACEnabled {
			return handler(ctx, req)
		}
		path := grpc_http.HttpPath(ctx)
		// For now, skip checking rbac for ops operations.
		if strings.HasPrefix(path, "/api/v1/serverless/ops/") {
			return handler(ctx, req)
		}

		var orgId, projectId uint64
		if err := checkUrlPathIDs(ctx, &orgId, &projectId); err != nil {
			return nil, err
		}

		method := grpc_http.HttpMethod(ctx)

		if strings.HasPrefix(path, "/api/v1/serverless") {
			if needInjectProjectId(path) {
				/* impersonated user with project_id=0 */
				segs := strings.Split(path, "/")
				path = strings.Join(
					segs[:6],
					"/",
				) + fmt.Sprintf(
					"/projects/%d/",
					projectId,
				) + strings.Join(
					segs[6:],
					"/",
				)
			}
			param := auth.AccessParam{
				Req: ctx,
				Metadata: map[string]any{
					auth.AccessMetadataPath:                path,
					auth.AccessMetadataMethod:              method,
					auth.AccessMetadataRequestWhitelistKey: RBACPathWhitelist,
					auth.AccessMetadataResourceMapKey:      DefaultRBACPathPattern2ResourceMap,
				},
			}
			result, err := permissionChecker.Handle(ctx, &param)
			if err != nil || result.Code != auth.AuthnRetOK {
				log.WithContextL(ctx).Error("RBAC check failed", zap.String("path", path),
					zap.String("method", method), zap.Any("result", result), zap.Error(err))
				return nil, errorForbidden
			}
		} else if strings.HasPrefix(path, "/v1beta1/") {
			// Check Open api privileges for /v1beta1
			resourceUri, err := getResource(ctx, method, path)
			if err != nil {
				return nil, err
			}

			t, ok := ctx.Value(config.CtxIdentityTypeKey).(auth.AuthnIdentityType)
			if !ok {
				log.WithContextL(ctx).Error("identity type not found", zap.String("identity_type", fmt.Sprintf("%v", ctx.Value(config.CtxIdentityTypeKey))))
				return nil, errorForbidden
			}
			v, ok := ctx.Value(config.CtxIdentityValKey).(string)
			if !ok {
				log.WithContextL(ctx).Error("identity val not found", zap.String("identity_val", fmt.Sprintf("%v", ctx.Value(config.CtxIdentityValKey))))
				return nil, errorForbidden
			}

			clusterId, found := grpc_http.GetClusterIdFromPath(path)
			if !found {
				log.WithContextL(ctx).Error("invalid url, cluster id not found", zap.String("path", path))
				return nil, errcode.NewInvalidInputErr(errors.New("invalid cluster id"))
			}

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

			// Need to dd all params because CheckIdentityPermission will check the inconsistency of url path and param
			if strings.Contains(resourceUri, RBACBranchIdKey) {
				branchId, found := grpc_http.GetBranchIdFromPath(path)
				if !found {
					log.WithContextL(ctx).Error("invalid url, branch id not found", zap.String("path", path), zap.String("resource_uri", resourceUri))
					return nil, errcode.NewInvalidInputErr(errors.New("invalid branch id"))
				}
				param[RBACBranchIdKey] = branchId
			}

			if strings.Contains(resourceUri, RBACExportIdKey) {
				exportId, found := grpc_http.GetExportIdFromPath(path)
				if !found {
					log.WithContextL(ctx).Error("invalid url, export id not found", zap.String("path", path), zap.String("resource_uri", resourceUri))
					return nil, errcode.NewInvalidInputErr(errors.New("invalid export id"))
				}
				param[RBACExportIdKey] = exportId
			}

			if strings.Contains(resourceUri, RBACImportIdKey) {
				importId, found := grpc_http.GetImportIdFromPath(path)
				if !found {
					log.WithContextL(ctx).Error("invalid url, import id not found", zap.String("path", path), zap.String("resource_uri", resourceUri))
					return nil, errcode.NewInvalidInputErr(errors.New("invalid import id"))
				}
				param[RBACImportIdKey] = importId
			}

			if strings.Contains(resourceUri, RBACMigrationTaskIdKey) {
				migrationTaskId, found := grpc_http.GetMigrationIdFromPath(path)
				if !found {
					log.WithContextL(ctx).Error("invalid url, migration task id not found", zap.String("path", path), zap.String("resource_uri", resourceUri))
					return nil, errcode.NewInvalidInputErr(errors.New("invalid migration task id"))
				}
				param["id"] = migrationTaskId
			}

			if strings.Contains(resourceUri, RBACMigrationPrecheckIdKey) {
				migrationPrecheckId, found := grpc_http.GetMigrationPrecheckIdFromPath(path)
				if !found {
					log.WithContextL(ctx).Error("invalid url, migration precheck id not found", zap.String("path", path), zap.String("resource_uri", resourceUri))
					return nil, errcode.NewInvalidInputErr(errors.New("invalid migration precheck id"))
				}
				param["id"] = migrationPrecheckId
			}

			var identityTye auth.AccessIdentityType
			switch t {
			case auth.AuthnIdentityTypeApiKeyAK:
				identityTye = auth.AccessIdentityTypeApiKeyAK
			case auth.AuthnIdentityTypeAccessToken:
				identityTye = auth.AccessIdentityTypeAccessToken
			default:
				log.WithContextL(ctx).Error("identity type invalid", zap.Any("type", t))
				return nil, errorForbidden
			}

			permission, err := auth.CheckIdentityPermission(ctx, &auth.CheckPermissionParam{
				IdentityTye:   identityTye,
				IdentityVal:   v,
				ResourceUri:   resourceUri,
				ResourceParam: param,
			})
			if err != nil {
				log.WithContextL(ctx).Error("check permission failed", zap.String("resource_uri", resourceUri), zap.Any("param", param), zap.Error(err))
				return nil, errcode.NewForbiddenErr(err)
			}
			if !permission.HasPermission {
				return nil, errorForbidden
			}
		} else if strings.HasPrefix(path, "/api/internal/") || strings.HasPrefix(path, "/api/v1beta/") {
			// old version Open APIs only support org owner API Key, no RBAC. access token is now only org owner and no CtxOpenApiRoles context key
			if ctx.Value(auth0.AuthTypeKey).(string) == config.ApiKeyAuthType {
				r, ok := ctx.Value(config.CtxOpenApiRoles).([]acl.OrgRole)
				if !ok {
					log.WithContextL(ctx).Error("api key roles not found", zap.String("path", path), zap.Any("roles", ctx.Value(config.CtxOpenApiRoles)))
					return nil, errorForbidden
				}
				if !slices.Contains(r, acl.OrgRole_ORG_ROLE_OWNER) {
					log.WithContextL(ctx).Error("check permission failed", zap.String("path", path), zap.Any("roles", r))
					return nil, errcode.NewForbiddenErr(errors.New("The v1beta API is restricted to the role of \"Organization Owner\" only. We recommend using the v1beta1 API for broader roles access."))
				}
			}
		}

		return handler(ctx, req)
	}
}

func getResource(ctx context.Context, method, path string) (string, error) {
	for regex, uri := range RBACOpenAPIPathPattern2ResourceMap {
		if regex.MatchString(fmt.Sprintf("%s:%s", strings.ToLower(method), path)) {
			return uri, nil
		}
	}

	log.WithContextL(ctx).Warn("get resource uri failed", zap.String("method", method), zap.String("path", path))
	return "", errorForbidden
}

func needInjectProjectId(path string) bool {
	if strings.Contains(path, "spend_limits") {
		return true
	}
	if strings.Contains(path, "resource_limit") {
		return true
	}
	if strings.Contains(path, "recycledInstances") && !recycledInstancesListPathPattern.MatchString(path) {
		return true
	}
	return false
}

func checkUrlPathIDs(ctx context.Context, orgId, projectId *uint64) error {
	user, err := utils.GetUserFromContext(ctx)
	if err != nil {
		return errcode.NewNotAuthenticatedErr(errors.New("not authenticated"))
	}
	path := grpc_http.HttpPath(ctx)

	if user.IsImpersonated {
		/* try to get project_id, without any check */
		/* method 1. get pid from url path */
		var found bool
		*projectId, found = grpc_http.GetProjectIdFromPath(path)
		if found {
			return nil
		}
		/* method 2. trying to get project_id from parameters */
		*projectId, found = grpc_http.GetProjectIdFromURI(grpc_http.HttpURI(ctx))
		if found {
			return nil
		}
		/* method 3. get project_id form mgmt_api.Cluster */
		if cid, found := grpc_http.GetClusterIdFromPath(path); found {
			if cluster, err := grpcli.GetCluster(ctx, cid); err == nil {
				*projectId = cluster.GetProjectId()
				return nil
			}
		}
		return nil
	}

	if oid, found := grpc_http.GetOrgIdFromPath(path); found {
		if err := checkOrgId(user, oid); err != nil {
			log.WithContextL(ctx).Error("check path org id failed", zap.Error(err))
			return errorForbidden
		}
		*orgId = oid
	}

	if pid, found := grpc_http.GetProjectIdFromPath(path); found {
		*projectId = pid
	}

	/* trying to get project_id from parameters */
	if pid, found := grpc_http.GetProjectIdFromURI(grpc_http.HttpURI(ctx)); found {
		*projectId = pid
	}

	cid, cidFound := grpc_http.GetClusterIdFromPath(path)
	if cidFound {
		cluster, err := grpcli.GetClusterIncludeDeleted(ctx, cid)
		if err != nil {
			if err.RawErrCode() == bizerr.ErrCodeRecordNotFound {
				return errcode.NewNotFoundErr(errors.New("cluster not found"))
			}
			log.WithContextL(ctx).Error("get cluster failed", zap.Uint64("cluster_id", cid), zap.Error(err))
			return errorForbidden
		}
		if err := checkOrgId(user, cluster.OrganizationId); err != nil {
			log.WithContextL(ctx).Error("check cluster org id failed", zap.Error(err))
			return errorForbidden
		}
		*projectId = cluster.GetProjectId()
		*orgId = cluster.OrganizationId
	}

	return nil
}

func checkOrgId(user *facade.User, orgId uint64) error {
	if user.TenantID == orgId {
		return nil
	}
	return errcode.NewForbiddenErr(fmt.Errorf("you have no permission to access org %d, userOrgID", orgId))
}
