package auth

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

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/internal-portal/core-portal/central/config"
	grpc_http "github.com/tidbcloud/internal-portal/core-portal/central/grpc/http"
	"github.com/tidbcloud/internal-portal/core-portal/central/model/testtoken"
	pkg_http "github.com/tidbcloud/internal-portal/core-portal/central/pkg/grpc/http"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/auth/util"
	"github.com/tidbcloud/pkgv2/log"
)

const (
	BillingCleanClusters        = "/core.CoreService/CleanClusters"
	BillingCleanProjectResource = "/core.CoreService/CleanProjectResource"
	BillingCleanTenantResource  = "/core.CoreService/CleanTenantResource"
	BillingListClusters         = "/core.CoreService/ListClusters"
	CoreSvcDeleteCluster        = "/core.CoreService/DeleteCluster"
)

var (
	SkipGRPCAuthMap = map[string]bool{
		BillingCleanClusters:        true,
		BillingCleanProjectResource: true,
		BillingCleanTenantResource:  true,
		BillingListClusters:         true,
		CoreSvcDeleteCluster:        true,
	}
)

var ImpersonatedPOSTURLWhiteList = []*regexp.Regexp{
	regexp.MustCompile(`.*/query$`),
}

func filterHttpRequest(httpMethod, url string) bool {
	switch httpMethod {
	case "GET", "OPTIONS":
		return false
	case "POST":
		for _, allowPattern := range ImpersonatedPOSTURLWhiteList {
			if allowPattern.MatchString(url) {
				return false
			}
		}
		return true
	default:
		return true
	}
}

// getBearerToken gets the bearer token string in `Bearer {xxx}` format in the Authorization header
func getBearerToken(ctx context.Context) (string, error) {
	bearToken := pkg_http.FirstHeader(ctx, "authorization")
	if bearToken != "" {
		tokenRaw, err := util.ExtractBearerToken(bearToken)
		if err != nil {
			newErr := errors.Annotate(err, "invalid authorization header")
			return "", newErr
		}
		return tokenRaw, nil
	}
	return grpc_http.Query(ctx, "jwt"), nil
}

func NewAuthFunc(tokenValidator *auth0.RawTokenValidator, testTokenModel *testtoken.Model) func(context.Context) (context.Context, error) {
	return func(ctx context.Context) (context.Context, error) {
		logger := log.WithContextS(ctx)
		grpcMethod := pkg_http.Method(ctx)
		// Skip: Some gRPC services used only by internal microservices components.
		if val, ok := SkipGRPCAuthMap[grpcMethod]; ok && val {
			return ctx, nil
		}

		httpURI := grpc_http.HttpURI(ctx)
		if strings.HasPrefix(httpURI, "/api/v1/test-token") {
			// Skip：Get test token.
			return ctx, nil
		}
		// Open API
		if strings.HasPrefix(httpURI, "/api/v1beta") {
			user, err := getApiKeyUser(ctx)
			if err != nil {
				return nil, errcode.NewNotAuthenticatedErr(errors.New("unauthorized"))
			}
			return context.WithValue(ctx, auth0.AuthTokenClaimKey, user), nil
		}
		// API Prefix: /api/v1/orgs/(\d+)/
		orgID, err := ParseOrgIDFromPath(httpURI)
		if err != nil {
			return nil, errcode.NewNotAuthenticatedErr(err)
		}

		token, err := getBearerToken(ctx)
		if err != nil {
			return nil, err
		}
		user, err := Auth(ctx, token, tokenValidator, testTokenModel, orgID)
		if err != nil {
			logger.Infof("auth failed: %s", err)
			return nil, errcode.NewNotAuthenticatedErr(errors.New("unauthorized"))
		}

		if user.IsImpersonated {
			httpMethod := strings.ToUpper(grpc_http.HttpMethod(ctx))
			httpMethodFromContext := ctx.Value(auth0.AuthHttpMethod)
			if httpMethodFromContext != nil {
				httpMethod = strings.ToUpper(httpMethodFromContext.(string))
			}

			if filterHttpRequest(httpMethod, httpURI) {
				return nil, errcode.NewForbiddenErr(errors.New(config.ErrImpersonatedUser))
			}
		}

		newCtx := context.WithValue(ctx, auth0.AuthValidatorIDKey, auth0.AuthValidatorIDCentral)
		newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, user)
		newCtx = context.WithValue(newCtx, auth0.AuthUserIDKey, user.Email)
		return newCtx, nil
	}
}

func Auth(ctx context.Context, token string, tokenValidator *auth0.RawTokenValidator, testTokenModel *testtoken.Model, orgID uint64) (*User, error) {
	var (
		email    string
		subClaim string
	)

	if testTokenModel != nil {
		email = testTokenModel.TryGetEmailFromTestToken(token)
	}
	if email == "" {
		claims, err := tokenValidator.ValidateFor(auth0.AuthValidatorIDCentral, token)
		if err != nil {
			return nil, err
		}

		email, err = auth0.GetEmailClaim(claims)
		if err != nil {
			return nil, err
		}

		subClaim, err = auth0.GetSubjectClaim(claims)
		if err != nil {
			return nil, err
		}
	}

	return GetUser(ctx, email, subClaim, orgID)
}

var (
	RegexpApiPrefix = `^/api/v1/orgs/(\d+)/`
)

func ParseOrgIDFromPath(urlPath string) (orgID uint64, err error) {
	re := regexp.MustCompile(RegexpApiPrefix)
	matches := re.FindStringSubmatch(urlPath)
	if matches == nil {
		return 0, fmt.Errorf("invalid URL: %s", urlPath)
	}

	orgID, err = strconv.ParseUint(matches[1], 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid URL: %s", urlPath)
	}

	return
}
