package centralsvc

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

	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errcode"
	"github.com/tidbcloud/metrics-server/central/config"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/db/tenant"
	grpc_http "github.com/tidbcloud/metrics-server/central/grpc/http"
	"github.com/tidbcloud/metrics-server/central/model/account/centralmod"
	"github.com/tidbcloud/metrics-server/central/model/account/utils"
	"github.com/tidbcloud/metrics-server/central/model/session"
	"github.com/tidbcloud/metrics-server/central/service"
	pkg_http "github.com/tidbcloud/metrics-server/pkg/grpc/http"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	tenantIDHeader = "X-Tenant-ID"
)

const (
	// The user has not registered in the system
	UnregisteredUserErrMsg = "unregistered user"

	// User has been deleted
	InactiveUserErrMsg = "inactive user"

	// The user did not accept the invitation in the email
	PendingUserErrMsg = "pending user"

	// The user has self-registered and has not activated the account in the email
	PendingSignupUserErrMsg = "pending signup user"
)

var ImpersonatedPOSTURLWhiteList = []*regexp.Regexp{
	regexp.MustCompile(`.*/query$`),
	regexp.MustCompile(`^/api_proxy/billing/api/v1/billing/tenants/(?P<tenant_id>\d+)/costexplorer.*$`),
}

type GetUserClaimFunc func(context.Context, jwt.MapClaims) (*tenant.User, error)

// ToGetClaim satisfies the IntoGetClaim interface
func (f GetUserClaimFunc) ToGetClaim() auth0.GetClaim {
	return func(ctx context.Context, claims jwt.MapClaims) (interface{}, error) {
		return f(ctx, claims)
	}
}

func MakeAuthGRPC(tokenValidator *auth0.RawTokenValidator, getClaim auth0.IntoGetClaim, testTokenModel centralmod.CoveredTestTokenModel) func(context.Context) (context.Context, error) {
	return func(ctx context.Context) (context.Context, error) {
		url := grpc_http.HttpURI(ctx)

		if strings.HasPrefix(url, "/debug-api/") {
			// Debug Portal service has its own authentication. Skip here.
			return ctx, nil
		}
		if strings.HasPrefix(url, "/api/v1/test-token") {
			//get test token, Skip here.
			return ctx, nil
		}

		method := pkg_http.Method(ctx)
		// if client call accept invite interface, use token make auth instead of google auth
		if val, ok := session.CheckMap[method]; ok && val {
			// the header can't parse parameters across grpc-gateway
			// token auth leave to the logic layer
			// TODO: research on grpc header
			return ctx, nil
		}
		tokenRaw, err := service.GetBearerTokenFromGrpc(ctx)
		if err != nil {
			return nil, errcode.NewNotAuthenticatedErr(err)
		}

		var claims jwt.MapClaims

		var validatorID = auth0.AuthValidatorIDCentral
		newCtx := context.WithValue(ctx, auth0.AuthValidatorIDKey, validatorID)

		orgID, found := utils.ParseOrgIDFromPath(url)
		testUser := testTokenModel.TryGetTestUserFromToken(tokenRaw, orgID, !found)
		var v interface{}
		if testUser == nil {
			claims, err = tokenValidator.ValidateFor(validatorID, tokenRaw)
			if err != nil {
				return nil, errcode.NewNotAuthenticatedErr(err)
			}

			if val, ok := session.CheckEmailMap[method]; ok && val {
				email, err := auth0.GetEmailClaim(claims)
				if err != nil {
					return nil, errcode.NewNotAuthenticatedErr(err)
				}
				user := entity.User{
					Email: email,
				}
				v = &tenant.User{
					User: user,
				}
			} else {
				v, err = getClaim.ToGetClaim()(ctx, claims)
				if err != nil {
					if _, ok := err.(errcode.ErrorCode); ok {
						return nil, err
					}
					return nil, errcode.NewNotAuthenticatedErr(err)
				}
			}
		} else {
			v = testUser
			log.Info(ctx, "login using test token", zap.String("email", testUser.Email))
		}

		// grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
		// grpc_ctxtags.Extract(ctx).Set("auth.sub", claims)
		newCtx = context.WithValue(newCtx, auth0.AuthTypeKey, auth0.GmailAuthType)
		newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, v)
		newCtx = context.WithValue(newCtx, auth0.AuthUserIDKey, claims["email"])
		return newCtx, nil
	}
}

func GetUserClaim(acceptBot ...bool) GetUserClaimFunc {
	return func(ctx context.Context, claims jwt.MapClaims) (*tenant.User, error) {
		email, err := auth0.GetEmailClaim(claims)
		if err != nil {
			if len(acceptBot) > 0 && !acceptBot[0] {
				return nil, errcode.NewNotAuthenticatedErr(err)
			}
			email, err = auth0.GetCustomEmailClaim(claims)
			if err != nil {
				return nil, errcode.NewNotAuthenticatedErr(err)
			}
		}
		url := grpc_http.HttpURI(ctx)
		if strings.HasPrefix(url, "/api/v1/admin/") {
			return centralAdminLogin(ctx, email)
		}
		orgID, found := utils.ParseOrgIDFromPath(url)
		u, found := centralmod.GetUser(email, orgID, !found)
		if !found {
			return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredUserErrMsg))
		}
		if u.Tenant != nil && (u.Tenant.Status == entity.TenantStatusCleared || u.Tenant.Status == entity.TenantStatusDeleted) {
			log.Warn(ctx, "tenant has been cleared/deleted", zap.Uint64("tenantID", u.TenantID.Uint64()), zap.String("status", string(u.Tenant.Status)))
			return nil, errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
		}
		// check the impersonated user first
		if u != nil && u.IsImpersonated {
			httpMethod := strings.ToUpper(grpc_http.HttpMethod(ctx))
			httpMethodFromContext := ctx.Value(auth0.AuthHttpMethod)
			if httpMethodFromContext != nil {
				httpMethod = strings.ToUpper(httpMethodFromContext.(string))
			}

			if filterHttpRequest(httpMethod, url) {
				return nil, errcode.NewForbiddenErr(errors.New(config.ErrImpersonatedUser))
			}
		}
		if u != nil && !u.IsImpersonated {
			switch u.Status {
			case entity.UserStatusActive:
				return u, nil
			case entity.UserStatusPending:
				if u.InvitedAt != nil {
					return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
				}
				if u.Email == u.Tenant.Email && u.Tenant.Status == entity.TenantStatusPending {
					return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingSignupUserErrMsg))
				}
				return nil, errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
			case entity.UserStatusInactive:
				return nil, errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
			}
		}
		return u, nil
	}
}

func centralAdminLogin(ctx context.Context, email string) (*tenant.User, error) {
	admin, found := tenant.GetUser(email)
	if !found {
		return nil, errcode.NewNotAuthenticatedErr(errors.New("email is not found"))
	}
	if !canAccessAdminEndpoint(admin, grpc_http.HttpURI(ctx), grpc_http.HttpMethod(ctx)) {
		return nil, errcode.NewForbiddenErr(errors.New("forbidden"))
	}
	tenantID, err := strconv.ParseUint(getTenantID(ctx), 10, 64)
	if tenantID > 0 && err == nil {
		log.Info(ctx, "login as tenant syxstem users", zap.String("email", email), zap.Uint64("tenant", tenantID))
		var tenant entity.Tenant
		err := db.Transaction(func(tx *gorm.DB) error {
			return entity.NewTenantQuerySet(tx).IDEq(entity.ID(tenantID)).One(&tenant)
		})
		if err != nil {
			return nil, err
		}
		admin.TenantID = tenant.ID
	}
	return admin, nil
}

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
	}
}

func canAccessAdminEndpoint(user *tenant.User, url string, method string) bool {
	return user.User.TenantID == entity.PingCAPTenantID && service.HasPrivilege(user.Roles, url, method)
}

func getTenantID(ctx context.Context) string {
	if id := pkg_http.FirstHeader(ctx, tenantIDHeader); id != "" {
		return id
	}
	return grpc_http.Cookie(ctx, tenantIDHeader)
}
