/******************************************************************************
 * Copyright (c)  2025 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.                                             *
 ******************************************************************************/

package interceptor

import (
	"bytes"
	"encoding/json"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/common/constants"
	o11yerrors "github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/globalservice"
	"go.uber.org/zap"
)

var internalTokenCache *cache.Cache
var internalAuthLock sync.Mutex

type tokenVerifyReq struct {
	Token string `json:"token"`
}

type tokenVerifyResp struct {
	Exp            int64  `json:"exp"`
	GithubUsername string `json:"github_username"`
	Iat            int64  `json:"iat"`
}

type cachedTokenInfo struct {
	GithubUsername string
	Exp            int64
}

func InitInternalAuthCache() {
	internalTokenCache = cache.New(constants.DefaultCacheTimeout, constants.DefaultCacheTimeout*2)
}

func InternalAuth(c *gin.Context) {
	if internalTokenCache == nil {
		log.Error("internal token cache not initialized")
		c.String(http.StatusInternalServerError, "internal auth not initialized")
		c.Abort()
		return
	}

	bearerTokenStr := c.GetHeader(constants.AuthorizationHeader)
	githubUsername, err := verifyInternalToken(bearerTokenStr)
	if err != nil {
		_ = c.Error(err)
		log.Error("internal auth failed", zap.Error(err))
		c.String(http.StatusUnauthorized, err.Error())
		c.Abort()
		return
	}

	c.Request.Header.Set(constants.HeaderUserId, githubUsername)
	c.Next()
}

func verifyInternalToken(bearerTokenStr string) (string, error) {
	if bearerTokenStr == "" {
		return "", o11yerrors.New(o11yerrors.ParameterInvalid, "authorization token empty")
	}
	if !strings.HasPrefix(bearerTokenStr, constants.BearerPrefix) {
		return "", o11yerrors.NewErrorf(o11yerrors.ParameterInvalid, "bearer token should begin with `%s`", constants.BearerPrefix)
	}

	token := strings.TrimSpace(bearerTokenStr[len(constants.BearerPrefix):])

	if cached, ok := internalTokenCache.Get(token); ok {
		info := cached.(cachedTokenInfo)
		if time.Now().Unix() < info.Exp-300 {
			return info.GithubUsername, nil
		}
		internalTokenCache.Delete(token)
	}

	internalAuthLock.Lock()
	defer internalAuthLock.Unlock()

	if cached, ok := internalTokenCache.Get(token); ok {
		info := cached.(cachedTokenInfo)
		if time.Now().Unix() < info.Exp-300 {
			return info.GithubUsername, nil
		}
		internalTokenCache.Delete(token)
	}

	resp, err := verifyTokenViaHTTP(token)
	if err != nil {
		return "", err
	}

	now := time.Now().Unix()
	cacheTTL := time.Duration(resp.Exp-now-300) * time.Second
	if cacheTTL <= 0 {
		cacheTTL = time.Minute
	}

	internalTokenCache.Set(token, cachedTokenInfo{
		GithubUsername: resp.GithubUsername,
		Exp:            resp.Exp,
	}, cacheTTL)

	return resp.GithubUsername, nil
}

func verifyTokenViaHTTP(token string) (*tokenVerifyResp, error) {
	metaCache := globalservice.GetGlobalManager().GetGlobalMetaCache()
	if metaCache == nil || metaCache.ServerConfig.InternalUserTokenVerifyURL == "" {
		return nil, o11yerrors.New(o11yerrors.UnrecognizedError, "internal user token verify URL not configured")
	}

	reqBody := tokenVerifyReq{Token: token}
	bodyBytes, _ := json.Marshal(reqBody)

	resp, err := http.Post(metaCache.ServerConfig.InternalUserTokenVerifyURL, "application/json", bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, o11yerrors.NewErrorf(o11yerrors.HttpClientResponseError, "token verify request failed: %s", err.Error())
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, o11yerrors.NewErrorf(o11yerrors.HttpClientResponseError, "token verify failed with status: %d", resp.StatusCode)
	}

	var result tokenVerifyResp
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, o11yerrors.NewErrorf(o11yerrors.UnmarshalError, "failed to decode verify response: %s", err.Error())
	}

	if result.GithubUsername == "" {
		return nil, o11yerrors.New(o11yerrors.ParameterInvalid, "token is invalid or missing github_username")
	}

	return &result, nil
}

func GetInternalTokenCache() *cache.Cache {
	return internalTokenCache
}

func ClearInternalTokenCache() {
	if internalTokenCache != nil {
		internalTokenCache.Flush()
	}
}
