package api

// all the middlewares should be placed here

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

	"github.com/form3tech-oss/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/admin/consts"
	"github.com/pingcap/dbaas/admin/errs"
	"github.com/pingcap/dbaas/admin/models"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/central/model/apikey"
	"github.com/pingcap/dbaas/central/rbac"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
)

var tokenValidator *auth0.RawTokenValidator

func InitTokenValidator(conf auth0.Conf) error {
	managementPortalJWKs, err := auth0.GetJWKS(conf.Domain)
	if err != nil {
		return err
	}

	tokenValidator = auth0.NewRawTokenValidator().
		RegisterAuth(auth0.AuthValidatorIDManagement, conf, managementPortalJWKs)

	return nil
}

func GetToken(c *gin.Context) (string, error) {
	header := c.GetHeader("authorization")
	if header == "" {
		/*
		   since HTTP/2 use a lower case header, HTTP/1.x header is case-insensitive,
		   but generally we use a Authorization header in HTTP/1.x
		   LINK:
		   https://tools.ietf.org/html/rfc7540#section-8.1.2
		   https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
		*/
		header = c.GetHeader("Authorization")
	}

	if header == "" {
		return "", errs.ErrTokenNotFound
	}

	tokenSlice := strings.Split(header, " ")
	if len(tokenSlice) != 2 || strings.ToLower(tokenSlice[0]) != "bearer" {
		return "", errs.ErrTokenNotFound
	}

	return tokenSlice[1], nil
}

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

func getUserFromContext(c *gin.Context) (*tenant.User, error) {
	rawToken, err := GetToken(c)
	if err != nil {
		return nil, err
	}

	var claims jwt.MapClaims
	testTokenEmail := models.GetTestTokenModel().TryGetEmailFromTestToken(rawToken)
	if testTokenEmail != "" {
		claims = jwt.MapClaims{}
		claims["email"] = testTokenEmail
		claims["gty"] = auth0.GmailAuthType
	} else {
		claims, err = tokenValidator.ValidateFor(auth0.AuthValidatorIDManagement, rawToken)
		if err != nil {
			return nil, err
		}
	}

	var user *tenant.User
	// m2m: machine to machine, which means, this is a RPC.
	if claims["gty"] == auth0.ClientCredentialsAuthType {
		entityUser := entity.User{
			Updatable: entity.Updatable{Base: entity.Base{ID: entity.PingCAPTenantID}},
			Email:     fmt.Sprintf("%v@m2m-client", claims["azp"]),
		}
		user = &tenant.User{User: entityUser}
	} else { // login as user
		var ok bool
		email, err := auth0.GetEmailClaim(claims)
		if err != nil {
			return nil, err
		}
		user, ok = tenant.GetUser(email)
		if !ok {
			return nil, errors.BadRequestf("can't get user: email=%s", email)
		}

		url := c.FullPath()
		method := c.Request.Method
		// normal user should have permission
		if !canAccessAdminEndpoint(user, url, method) {
			return nil, errs.ErrInvalidToken
		}
	}

	return user, nil
}

func MustHaveAdminPermissions() gin.HandlerFunc {
	return func(c *gin.Context) {
		user, err := getUserFromContext(c)
		if err != nil {
			log.Error("failed to get user from context", zap.Error(err))
			Fail(c, errs.ErrUnauthorized, http.StatusUnauthorized)
			c.Abort()
			return
		}
		c.Set(consts.CONTEXT_CURRENT_USER, user)

		// it's ok, let go!
		c.Next()
	}
}

func MustHaveAPIKey() gin.HandlerFunc {
	return func(c *gin.Context) {
		token, err := GetToken(c)
		if err != nil {
			log.Error("failed to get token", zap.Error(err))
			Fail(c, err, http.StatusUnauthorized)
			c.Abort()
			return
		}

		key, err := apikey.AuthenticateAPIKey(token)
		if err != nil {
			log.Error("failed to get key from token", zap.Error(err))
			Fail(c, err, http.StatusInternalServerError)
			c.Abort()
			return
		}
		if key == nil || key.ID == 0 {
			log.Error("failed to get key, key is nil", zap.Error(err))
			Fail(c, errs.ErrInvalidToken, http.StatusUnauthorized)
			c.Abort()
			return
		}
		c.Set(consts.CONTEXT_API_KEY, key)

		c.Next()
	}
}

func SetFakeUnitTestAPIKey(key *entity.APIKey) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set(consts.CONTEXT_API_KEY, key)

		c.Next()
	}
}

func SetFakeUnitTestUser(user *tenant.User) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set(consts.CONTEXT_CURRENT_USER, user)

		// it's ok, let go!
		c.Next()
	}
}
