package auth

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/juju/errors"
	"github.com/pingcap/log"

	entity "github.com/tidbcloud/core-svc/model"
	coreauth "github.com/tidbcloud/internal-portal/core-portal/auth"
	"github.com/tidbcloud/internal-portal/core-portal/central/config"
	"github.com/tidbcloud/internal-portal/core-portal/central/model/testtoken"
	"github.com/tidbcloud/internal-portal/temporary/utils"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/auth/util"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type Auth struct {
	conf           auth0.Conf
	tokenValidator *auth0.RawTokenValidator
	tokenModel     testtoken.Model
}

// InitAuth initialize auth as an authentication client
func InitAuth(conf config.Auth0Config, aesCrypt *encrypt.AESCrypt, k8sClient client.Client) (*Auth, error) {
	a := &Auth{
		conf: auth0.Conf{
			Audience: conf.Auth0Audience,
			Secret:   []byte(conf.Auth0Secret),
			Domain:   conf.Auth0Domain,
			// ClientCredentialAudience: conf.ManagementPortalAuth0ClientAudience,
		},
	}

	a.tokenModel = testtoken.NewModel(log.L(),
		conf.Auth0Domain,
		conf.Auth0ManagementClientID,
		conf.Auth0ManagementClientSecret,
		aesCrypt,
		k8sClient)

	jwks, err := auth0.GetJWKS(a.conf.Domain)
	if err != nil {
		return a, errors.Trace(err)
	}

	a.tokenValidator = auth0.NewRawTokenValidator().
		RegisterAuth(auth0.AuthValidatorIDCentral, a.conf, jwks)

	return a, nil

}

// Auth gin middleware
func (a *Auth) Auth(c *gin.Context) {
	// auth for other requests
	auth(c, a.tokenValidator, a.tokenModel)
}

// auth can be used in a http middleware
func auth(c *gin.Context, tokenValidator *auth0.RawTokenValidator, testTokenModel testtoken.Model) {

	noToken := false
	tokenRaw, err := getBearerToken(c)
	if err != nil || tokenRaw == "" {
		noToken = IsAuthWhitelist(c)
		if !noToken {
			authError(c, "Unauthorized")
			return
		}
	}

	path := c.Request.URL.Path
	orgID := utils.ParseOrgIDFromPath(path)

	var user *coreauth.User

	if noToken {
		user = &coreauth.User{
			ID:    1,
			Email: "internal@pingcap.com",
			Tenant: &coreauth.Tenant{
				ID: entity.ID(orgID),
			},
		}
	} else {
		user, err = AuthFunc(c, tokenRaw, tokenValidator, &testTokenModel, orgID)
		if err != nil {
			authError(c, err.Error())
			return
		}
	}

	c.Set(string(auth0.AuthTypeKey), auth0.GmailAuthType)
	c.Set(string(auth0.AuthTokenClaimKey), user)
	c.Set(string(auth0.AuthUserIDKey), user.Email)
}

func AuthFunc(c *gin.Context, token string, tokenValidator *auth0.RawTokenValidator, testTokenModel *testtoken.Model, orgID uint64) (*coreauth.User, error) {
	var (
		email    string
		subClaim string
	)

	if testTokenModel != nil {
		email = testTokenModel.TryGetEmailFromTestToken(token)
	}
	if email == "" {
		var validatorID = auth0.AuthValidatorIDCentral
		c.Set(string(auth0.AuthValidatorIDKey), validatorID)

		claims, err := tokenValidator.ValidateFor(validatorID, token)
		if err != nil {
			return nil, err
		}

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

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

	return coreauth.GetUser(c, email, subClaim, orgID)
}

// getBearerToken get the bearer token string in `Bearer {xxx}` format in the Authorization header
func getBearerToken(c *gin.Context) (string, error) {
	bearToken := c.Request.Header.Get("authorization")
	if bearToken != "" {
		tokenRaw, err := util.ExtractBearerToken(bearToken)
		if err != nil {
			newErr := errors.Annotate(err, "Auth Header invalid")
			return "", newErr
		}
		return tokenRaw, nil
	}
	return c.Query("jwt"), nil
}

func authError(c *gin.Context, msg string) {
	resp := map[string]string{"error": msg}
	c.JSON(http.StatusUnauthorized, resp)
	c.Abort()
}

func IsAuthWhitelist(c *gin.Context) bool {
	// skip authentication for certain API calls, as debug-portal
	// does not use Auth0 for user authentication, and we do not
	// have a proper way to get the same auth result as it.
	// The `GET /api/useralerts/v1/:organization/features` API call
	// exposes whitelisted feature list of a given tenant, and the
	// `GET /api/useralerts/v1/:organization/:cluster/authorization`
	// call exposes weither a given cluster has an valid auth.
	handlerName := strings.Split(c.HandlerName(), ".")
	if len(handlerName) > 0 {
		switch handlerName[len(handlerName)-1] {
		case "getUserDebugAuthCode",
			"getUserDebugFeatures":
			return true
		}
	}
	return false
}
