/******************************************************************************
 * Copyright (c)  2022 PingCAP, Inc.                                          *
 * Licensed under the Apache License, Version 2.0 (the "License");            *
 * you may not use this file except in compliance with the License.           *
 * You may obtain a copy of the License at                                    *
 *                                                                            *
 * http://www.apache.org/licenses/LICENSE-2.0                                 *
 *                                                                            *
 * Unless required by applicable law or agreed to in writing, software        *
 * distributed under the License is distributed on an "AS IS" BASIS,          *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 * See the License for the specific language governing permissions and        *
 * limitations under the License.                                             *
 ******************************************************************************/

/*******************************************************************************
 * @File: auth.go
 * @Description:
 * @Author: jiangxunyu@pingcap.com
 * @Version: 1.0.0
 * @Date: 2022/6/8 16:56
*******************************************************************************/

package interceptor

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/auth0/go-jwt-middleware/v2/jwks"
	"github.com/auth0/go-jwt-middleware/v2/validator"
	"github.com/gin-gonic/gin"
	"github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/globalservice"
	"go.uber.org/zap"
)

var jwtValidator *validator.Validator
var tokenCache *cache.Cache
var lock sync.Mutex

type RegisteredClaimsInfo struct {
	Issuer   string   `json:"Issuer"`
	Subject  string   `json:"Subject"`
	Audience []string `json:"audience"`
	Expiry   int64    `json:"Expiry"`
}

// InitAuthValidator
// @Description: init auth validator
// @return error
func InitAuthValidator() error {
	issuerURL, err := url.Parse(globalservice.GetGlobalManager().GetGlobalMetaCache().Auth0.IssuerURL)
	if err != nil {
		return fmt.Errorf("failed to parse the issuer url: %v", err.Error())
	}
	authCachingProvider := jwks.NewCachingProvider(issuerURL, constants.DefaultCacheTimeout)
	// set up the jwt token validator.
	jwtValidator, err = validator.New(
		authCachingProvider.KeyFunc,
		validator.RS256,
		globalservice.GetGlobalManager().GetGlobalMetaCache().Auth0.IssuerURL,
		[]string{globalservice.GetGlobalManager().GetGlobalMetaCache().Auth0.Audience},
	)
	if err != nil {
		return fmt.Errorf("failed to set up the validator. error: %s", err.Error())
	}

	tokenCache = cache.New(constants.DefaultCacheTimeout, constants.DefaultCacheTimeout*2)
	return nil
}

// Auth
// @Description: global control plane token authentication interceptor
// @Parameter c
func Auth(c *gin.Context) {
	bearerTokenStr := c.GetHeader(constants.AuthorizationHeader)
	if err := verifyIdentity(bearerTokenStr); err != nil {
		_ = c.Error(err)
		log.Error("failed to verify identity", zap.Error(err))
		c.String(http.StatusUnauthorized, err.Error())
		c.Abort()
	} else {
		c.Next()
	}
}

// verifyIdentity
// @Description: request central identity services for authentication
// @return error
func verifyIdentity(bearerTokenStr string) error {
	if bearerTokenStr == "" {
		return errors.New("authorization token empty")
	}
	if !strings.HasPrefix(bearerTokenStr, constants.BearerPrefix) {
		return fmt.Errorf("bearer token %s should begin with `%s`", bearerTokenStr, constants.BearerPrefix)
	}

	token := strings.TrimSpace(bearerTokenStr[len(constants.BearerPrefix):])
	log.Debug("current cache auth token item size",
		zap.Int("Token_Item_Number", tokenCache.ItemCount()),
	)
	// cache validate
	if exp, ok := tokenCache.Get(token); ok {
		if time.Now().Unix() < exp.(int64) {
			// cache validate success
			return nil
		}
	}

	lock.Lock()
	defer lock.Unlock()

	// double check lock
	if exp, ok := tokenCache.Get(token); ok {
		if time.Now().Unix() < exp.(int64) {
			// cache validate success
			return nil
		}
	}

	// validate token
	return validateJwtToken(token)
}

// validateJwtToken
// @Description: validate auth0 jwt token
// @Parameter token
// @return error
func validateJwtToken(token string) error {
	validateToken, err := jwtValidator.ValidateToken(context.TODO(), token)
	if err != nil {
		return fmt.Errorf("failed to validate token. error: %s", err.Error())
	}
	expireToken(token, validateToken.(*validator.ValidatedClaims))
	return nil
}

// expireToken
// @Description: token expiry and token cached
// @Parameter token
// @Parameter validateToken
// @return error
func expireToken(token string, validateToken *validator.ValidatedClaims) {
	tokenExpiry := validateToken.RegisteredClaims.Expiry
	// set cache, the default cache expiry time is the difference between the token expiry time and the current time minus 60
	expire := time.Duration(tokenExpiry-time.Now().Unix()-60) * time.Second
	if expire <= 0 {
		expire = constants.DefaultCacheTimeout
	}
	tokenCache.Set(token, tokenExpiry, expire)
}

// GCTokenCache
// @Description: global control plane token cache garbage collection
// @Parameter ctx
func GCTokenCache(ctx context.Context) {
	timer := time.NewTimer(constants.GlobalCacheGC)
loop:
	for {
		select {
		case <-ctx.Done():
			break loop
		case <-timer.C:
		}
		timer.Reset(constants.GlobalCacheGC)
		tokenCache.DeleteExpired()
		log.Info("after GC",
			zap.Int("Auth_Cache_Size", tokenCache.ItemCount()),
		)
	}
}
