package api

// all the middlewares should be placed here

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	entity "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/internal-portal/core-portal/admin/consts"
	"github.com/tidbcloud/internal-portal/core-portal/admin/errs"
	"github.com/tidbcloud/internal-portal/core-portal/auth"
	"github.com/tidbcloud/internal-portal/core-portal/central/db"
	"github.com/tidbcloud/internal-portal/core-portal/central/grpc/grpcli"
	"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) (*auth.User, error) {
	token, err := GetToken(c)
	if err != nil {
		return nil, err
	}

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

	// Auth0 m2m
	if claims["gty"] == auth0.ClientCredentialsAuthType {
		user := &auth.User{
			Tenant: &auth.Tenant{
				ID: entity.PingCAPTenantID,
			},
			Email: fmt.Sprintf("%v@m2m-client", claims["azp"]),
		}
		return user, nil
	}

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

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

	orgID := entity.PingCAPTenantID.Uint64()
	user, err := auth.GetUser(c.Request.Context(), email, subClaim, orgID)
	if err != nil {
		return nil, err
	}
	err = adminAuthz(c, user)
	if err != nil {
		return nil, err
	}
	return user, nil
}

func adminAuthz(c *gin.Context, user *auth.User) error {
	req := &acl.HasAdminAccessReq{
		UserId: user.ID.Uint64(),
		Uri:    c.FullPath(),
		Method: strings.ToUpper(c.Request.Method),
	}
	resp, err := grpcli.HasAdminAccess(c.Request.Context(), req)
	if err != nil {
		return err
	}
	if !resp.HasAccess {
		return errs.ErrUnauthorized
	}
	return nil
}

func MustHaveAdminPermissions() gin.HandlerFunc {
	return func(c *gin.Context) {
		user, err := adminAuth(c)
		if err != nil {
			log.Error("unauthorized", 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 := 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
}
