package auth

import (
	"errors"
	"net/http"
	"regexp"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	centralapi "github.com/tidbcloud/core-svc/idl/pbgen/proto/central"
	"github.com/tidbcloud/idl/pbgen/proto/openapi"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/utils"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

// Auth is a middleware to authenticate the request.
//
// If the request is authenticated, it should set the following headers to the request:
// - Grpc-Metadata-X-Org-Id
// - Grpc-Metadata-X-User-Id
// - Grpc-Metadata-X-Default-Project-Id
// - Grpc-Metadata-X-Public-Key or Grpc-Metadata-X-User-Email
// - Grpc-Metadata-X-Debug-Trace-Id: trace id for debug request
//
// otherwise, it should abort the request with the unauthorized status code.
const (
	GrpcGatewayOrgIDKey            = "Grpc-Metadata-X-Org-Id"
	GrpcGatewayUserIDKey           = "Grpc-Metadata-X-User-Id"
	GrpcGatewayUserEmailKey        = "Grpc-Metadata-X-User-Email"
	GrpcGatewayPublicKeyKey        = "Grpc-Metadata-X-Public-Key"
	GrpcGatewayDefaultProjectIDKey = "Grpc-Metadata-X-Default-Project-Id"
	GrpcGatewayProjectIDListKey    = "Grpc-Metadata-X-Project-Id-List"
	GrpcGatewayClusterIDKey        = "Grpc-Metadata-X-Cluster-Id"
)

const (
	ProjectStatusActive  = "active"
	ProjectStatusDeleted = "deleted"
	ProjectStatusCleared = "cleared"
)

type User struct {
	PublicKey string
	User      *account.User
	Org       *account.Org
	Projects  []*account.Project
}

func SetGrpcMetaDataHeader() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		a, err := tigin.GetCurrentAuthInfo(ctx)
		if err != nil {
			tilog.Error(ctx.Request.Context(), "failed to get auth info", zap.Error(err))
			_ = ctx.AbortWithError(http.StatusForbidden, errors.New("failed to get auth info"))
			return
		}
		var user *User
		switch a.AuthType {
		case tigin.CURRENT_USER:

			// check isImpersonated
			if a.AuthUser != nil && a.AuthUser.IsImpersonated {
				if filterHttpRequest(ctx.Request.Method, ctx.Request.URL.Path) {
					// base error
					errMsg := &centralapi.DefaultResp{
						BaseResp: &centralapi.BaseResp{
							ErrCode: int64(bizerr.ErrCodeAuth),
							ErrMsg:  "impersonated user is read-only user",
						},
					}
					ctx.AbortWithStatusJSON(http.StatusOK, errMsg)
					return
				}
			}

			user, err = getAuth0User(ctx, a.AuthUser)
			if err != nil {
				tilog.Error(ctx.Request.Context(), "failed to get user by auth0", zap.Error(err))
				// base error
				errMsg := &centralapi.DefaultResp{
					BaseResp: &centralapi.BaseResp{
						ErrCode: int64(bizerr.ErrCodeAuth),
						ErrMsg:  "failed to get user by auth0",
					},
				}
				ctx.AbortWithStatusJSON(http.StatusOK, errMsg)
				return
			}
		case tigin.CURRENT_APIKEY:
			user, err = getApiKeyUser(ctx, a.OpenAPIAccess)
			if err != nil {
				tilog.Error(ctx.Request.Context(), "failed to get user by api key", zap.Error(err))
				errMsg := &openapi.ErrorResponse{
					Code:    int32(bizerr.ErrCodePermission),
					Message: err.Error(),
				}
				ctx.AbortWithStatusJSON(http.StatusForbidden, errMsg)
				return
			}
		}

		ctx.Request.Header.Set(GrpcGatewayOrgIDKey, strconv.FormatUint(user.Org.Id, 10))
		ctx.Request.Header.Set(GrpcGatewayUserIDKey, strconv.FormatUint(user.User.Id, 10))
		ctx.Request.Header.Set(GrpcGatewayUserEmailKey, user.User.Email)
		ctx.Request.Header.Set(GrpcGatewayPublicKeyKey, user.PublicKey)
		defaultProjectId, err := getDefaultProjectId(user)
		if err != nil {
			tilog.Error(
				ctx.Request.Context(),
				"failed to get default project id",
				zap.String("type", "api key"),
				zap.Error(err),
			)
			_ = ctx.AbortWithError(http.StatusForbidden, errors.New("failed to get default project id"))
			return
		}
		ctx.Request.Header.Set(GrpcGatewayDefaultProjectIDKey, strconv.FormatUint(defaultProjectId, 10))
		ctx.Request.Header.Set(GrpcGatewayProjectIDListKey, getProjectIdList(user))

		if clusterId, _ := utils.ParseClusterIDFromPath(ctx.Request.URL.Path); clusterId != 0 {
			ctx.Request.Header.Set(GrpcGatewayClusterIDKey, strconv.FormatUint(clusterId, 10))
		}
	}
}

var ImpersonatedPOSTURLWhiteList = []*regexp.Regexp{
	regexp.MustCompile(`.*/query$`),
}

func filterHttpRequest(httpMethod, url string) bool {
	switch httpMethod {
	case http.MethodGet, http.MethodOptions:
		return false
	case http.MethodPost:
		for _, allowPattern := range ImpersonatedPOSTURLWhiteList {
			if allowPattern.MatchString(url) {
				return false
			}
		}
		return true
	default:
		return true
	}
}
