package api

// all the middlewares should be placed here

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strings"

	"github.com/tidbcloud/account/central/utils"

	"github.com/tidbcloud/account/commons/config"

	"github.com/duke-git/lancet/v2/slice"

	"github.com/spf13/viper"

	"github.com/form3tech-oss/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/admin/consts"
	"github.com/tidbcloud/account/admin/errs"
	"github.com/tidbcloud/account/admin/models"
	"github.com/tidbcloud/account/internel/dao/entity"
	devops_handler "github.com/tidbcloud/account/thirdparty/devops"
	auth "github.com/tidbcloud/devops/idl/proto/devops_server/auth/v1"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var tokenValidator *auth0.RawTokenValidator

var akMapLocal = map[string]string{
	"ak_devops_dev": "dev_cdb6cfba-3dee-4175-a064-f7fc9ca90285",
	"ak":            "sk",
}

//var authenticator = utils.NewServerAuthenticator()

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) {
	ctx := c.Request.Context()
	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 == "" {
		tilog.Info(ctx, "not found Bearer token in the header")
		return "", errs.ErrTokenNotFound
	}

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

	return tokenSlice[1], nil
}

func adminAuth(c *gin.Context) (string, error) {
	email := ""
	var err error
	ctx := c.Request.Context()
	rawToken, _ := GetToken(c)

	if rawToken == "" {
		tilog.Info(ctx, "Authorization in the header is empty, will verify ak sk")
		akMap := getAkMap(ctx)
		authenticator := utils.GetAuthenticator(ctx)
		flag := authenticator.ValidateRequest(ctx, c.Request, akMap)
		if !flag {
			tilog.Info(ctx, "ak or sk is invalid")
			return "", errs.ErrTokenNotFound
		}

		// ak auth, client set X-Requester-Email in the header
		email = c.GetHeader("X-Requester-Email")

	} else {
		email, err = getAuth0Email(ctx, rawToken)
		//  Auth0 m2m return
		if strings.Contains(email, "@m2m-client") {
			return email, nil
		}

	}
	if err != nil {
		return "", err
	}

	if config.IsLocalEnv() {
		return email, nil
	}
	resp, err := devops_handler.GetHandler().GetUser(c, &auth.GetUserReq{Email: email})
	if err != nil {

		tilog.Info(ctx, "request DevopsAPI GetUser "+email, zap.Any("resp", resp))
		return "", errors.BadRequestf("can't get user from DevopsAPI : email=%s", email)
	}
	err = devopsRbac(c, resp.User.Email)
	tilog.Info(ctx, fmt.Sprintf("devopsRbac result = %t", err == nil))
	if err != nil {
		tilog.Info(ctx, "authz err", zap.Error(err))
		return "", err
	}
	return resp.User.Email, nil
}

func getAuth0Email(ctx context.Context, rawToken string) (string, error) {
	var claims jwt.MapClaims
	var err error
	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 {
			tilog.Info(ctx, "tokenValidator err", zap.Error(err))
			return "", err
		}
	}

	// Auth0 m2m
	if claims["gty"] == auth0.ClientCredentialsAuthType {
		str := fmt.Sprintf("%v@m2m-client", claims["azp"])
		return str, nil
	}

	// login as user
	email, err := auth0.GetEmailClaim(claims)
	if err != nil {
		tilog.Info(ctx, "auth0.GetEmailClaim err", zap.Error(err))
		return "", err
	}
	return email, nil
}

func devopsRbac(c *gin.Context, email string) error {
	ctx := c.Request.Context()
	// Check User Access
	req := &auth.HasAccessReq{
		Email:  email,
		Uri:    c.FullPath(),
		Method: strings.ToUpper(c.Request.Method),
	}
	if config.IsLocalEnv() {
		return nil
	}
	// if path in RbacPathWhitelist, no need to call devops to verify rbac.
	for _, subPath := range RbacPathWhitelist {

		pattern := regexp.MustCompile(subPath)
		isMatch := pattern.MatchString(req.Uri)

		if isMatch {
			tilog.Info(ctx, "the req.url isMatch rbac path whitelist. No need to call devops to verify rbac ", zap.String("subPath", subPath), zap.String("req.Uri", req.Uri))
			return nil
		}
	}
	adminUsers := viper.GetStringSlice("ops-admin.adminUsers")
	slice.Contain(adminUsers, email)
	if slice.Contain(adminUsers, email) {
		tilog.Info(ctx, email+" is admin user,skip authz")
	}

	resp, err := devops_handler.GetHandler().HasAccess(c.Request.Context(), req)
	tilog.Info(ctx, "user HasAccess result (request devops)", zap.Any("req", req), zap.Any("resp", resp))
	if err != nil {
		tilog.Info(ctx, "devops HasAccess api result err ", zap.Error(err))
		return err
	}
	if !resp.HasAccess {
		tilog.Info(ctx, "HasAccess result = false", zap.Any("email", email), zap.Any("req", req))
		return errs.ErrForbidden
	}
	return nil
}

func MustHaveAdminPermissions() gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := c.Request.Context()

		email, err := adminAuth(c)
		if err != nil {
			//tilog.Info(ctx, "failed to get user from context")
			if err == errs.ErrForbidden {
				Fail(c, errs.ErrForbidden, http.StatusForbidden)
				c.Abort()
				return
			}
			Fail(c, errs.ErrUnauthorized, http.StatusUnauthorized)
			c.Abort()
			return
		}
		c.Set(string(auth0.AuthTokenClaimKey), email)

		body := GenerateRequestBody(c)
		method := c.Request.Method
		msg := fmt.Sprintf("request user=%s", email)
		if method != "GET" {
			msg = msg + fmt.Sprintf(", body param=%s", body)
		}
		tilog.Info(ctx, msg)
		// it's ok, let go!
		c.Next()

	}
}

func GenerateRequestBody(c *gin.Context) string {
	ctx := c.Request.Context()
	body, err := c.GetRawData() //
	if err != nil {
		tilog.Info(ctx, "failed to get request body")
	}
	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
	return string(body)
}

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

		c.Next()
	}
}

func SetFakeUnitTestUser(email string) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set(string(auth0.AuthTokenClaimKey), email)

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

func getAkMap(ctx context.Context) map[string]string {
	if config.IsLocalEnv() {
		return akMapLocal
	}
	akMap := viper.GetStringMapString("ops-admin.ak-sk")

	//tilog.Info(ctx, "get akMap from viper", zap.Any("akMap", akMap))
	return akMap
}
