package api

// all the middlewares should be placed here

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/core-portal/internal/admin/consts"
	"github.com/tidbcloud/core-portal/internal/admin/errs"
	"github.com/tidbcloud/core-portal/internal/central/db"
	"github.com/tidbcloud/core-portal/internal/central/grpc/grpcli"
	entity "github.com/tidbcloud/core-svc/model"
	auth_proto "github.com/tidbcloud/devops/idl/proto/devops_server/auth/v1"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const BearerAPIKeyPrefix = "APIKEY_"

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
}

// GetToken ... get token from Authorization header of request. The token is the auth0 JWT or test token for QA.
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 an 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 adminAuth(c *gin.Context) (string, error) {
	token, err := GetToken(c)
	if err != nil {
		return "", err
	}

	var (
		email string
	)
	claims, err := tokenValidator.ValidateFor(auth0.AuthValidatorIDManagement, token)
	if err != nil {
		return "", err
	}

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

	// Auth0 login user
	email, err = auth0.GetEmailClaim(claims)
	if err != nil {
		return "", err
	}

	user, err := grpcli.GetUser(c.Request.Context(), &auth_proto.GetUserReq{
		Email: email,
	})
	if err != nil {
		return "", err
	}
	err = adminAuthz(c, user.User.Email)
	if err != nil {
		return "", err
	}
	return user.User.Email, nil
}

func adminAuthz(c *gin.Context, email string) error {
	req := &auth_proto.HasAccessReq{
		Email:  email,
		Uri:    c.FullPath(),
		Method: strings.ToUpper(c.Request.Method),
	}
	resp, err := grpcli.HasAccess(c.Request.Context(), req)
	if err != nil {
		return err
	}
	if !resp.HasAccess {
		return errs.ErrForbidden
	}
	return nil
}

func MustHaveAdminPermissions() gin.HandlerFunc {
	return func(c *gin.Context) {
		email, err := adminAuth(c)
		if err != nil {
			log.Error("unauthorized", zap.Error(err))
			if errors.Is(err, errs.ErrForbidden) {
				Fail(c, errs.ErrForbidden, http.StatusForbidden)
				c.Abort()
				return
			}
			Fail(c, err, http.StatusUnauthorized)
			c.Abort()
			return
		}

		c.Set(consts.CONTEXT_CURRENT_USER, email)

		// 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 := 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 hashKey(plainKey string) string {
	keyHashBytes := sha256.Sum256([]byte(plainKey))
	return hex.EncodeToString(keyHashBytes[:])
}

// AuthenticateAPIKey receives an API key in plain text and checks whether API key is valid.
// Note that this function does not do authorization, i.e. it does not verify whether API key matches the tenant.
func AuthenticateAPIKey(plainAPIKey string) (*entity.APIKey, error) {
	if !strings.HasPrefix(plainAPIKey, BearerAPIKeyPrefix) {
		return nil, nil
	}
	plainAPIKey = plainAPIKey[len(BearerAPIKeyPrefix):]
	var h entity.APIKey

	err := db.Transaction(func(tx *gorm.DB) error {
		if err := entity.
			NewAPIKeyQuerySet(tx).
			KeyHashEq(hashKey(plainAPIKey)).
			One(&h); err != nil {
			if gorm.ErrRecordNotFound == err {
				return nil
			}
			return err
		}
		return nil
	})

	if err != nil {
		return nil, err
	}
	return &h, nil
}
