package api

import (
	"context"
	"dataapps/commons/contextvar"
	"dataapps/commons/grpc"
	"dataapps/commons/logger"
	"dataapps/consts"
	"dataapps/errs"
	dto "dataapps/internal/dto/sqleditor"
	service "dataapps/internal/service/sqleditor"
	"dataapps/internal/service/util"
	"dataapps/metrics"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	errors "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"
)

// @Time    : 2022/11/14 4:28 PM
// @Author  : lvhongmeng

const (
	MAX                               = 5 * 1024 * 1024
	BOT_MAX                           = 1024 * 1024
	maxSQLClusterStatusIDs            = 100
	sqlClusterStatusPermissionTimeout = 5 * time.Second
)

func CheckUser(c *gin.Context) {

	ctx := c.Request.Context()
	logger := contextvar.Logger(ctx)
	if auth0Token, err := GetToken(c); err != nil {
		c.AbortWithError(http.StatusUnauthorized, err)
		return
	} else {
		c.Set(consts.CONTEXT_AUTH0_TOKEN, auth0Token)
		logger = logger.With(zap.String("auth0TokenMd5", util.Md5(auth0Token)))
	}
	testUser, ok := testGetToken(c)
	if ok {
		c.Set(consts.CONTEXT_CURRENT_USER, testUser)
		c.Set(consts.CURRENT_USER, testUser.User)
	} else {
		authUser, done := authorization(c)
		if done {
			c.Abort()
			return
		}
		c.Set(consts.CONTEXT_CURRENT_USER, authUser)
		c.Set(consts.CURRENT_USER, authUser.User)
		logger = logger.With(zap.String("user", authUser.Email))
		ctx = contextvar.WithAccountID(ctx, authUser.Email)
	}
	ctx = contextvar.WithLogger(ctx, logger)
	c.Request = c.Request.WithContext(ctx)
	c.Next()

}

func CheckSQLClusterStatusPermission(accountClient account.AccountAPIServiceClient) gin.HandlerFunc {
	return checkSQLClusterStatusPermissionWithTimeout(accountClient, sqlClusterStatusPermissionTimeout)
}

func checkSQLClusterStatusPermissionWithTimeout(accountClient account.AccountAPIServiceClient, timeout time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.Request.Method != http.MethodGet || c.FullPath() != sqlEditorClusterStatusRoutePattern {
			c.Next()
			return
		}

		clusterIDs, err := parseSQLClusterStatusClusterIDs(c.Query("cluster_ids"))
		if err != nil {
			tigin.AbortWithError(c, err)
			return
		}

		user, err := tigin.GetCurrentUser(c)
		if err != nil {
			tigin.AbortWithError(c, errors.NewForbidden("permission denied", err))
			return
		}
		permissionContext, cancel := context.WithTimeout(c.Request.Context(), timeout)
		defer cancel()

		for _, clusterID := range clusterIDs {
			resourceURI := fmt.Sprintf(
				"orgs/%s/projects/%s/tidbs/%d/sqleditor/get_api",
				c.Param("org_id"),
				c.Param("project_id"),
				clusterID,
			)
			response, permissionErr := accountClient.HasPermissionForUser(permissionContext, &account.RbacPermissionRequest{
				UserId:      user.Id,
				ResourceUri: resourceURI,
			})
			if permissionErr != nil || response == nil || !response.GetAuthResult() {
				tigin.AbortWithError(c, errors.NewForbidden("permission denied", permissionErr))
				return
			}
		}

		c.Next()
	}
}

func parseSQLClusterStatusClusterIDs(rawClusterIDs string) ([]uint64, error) {
	if rawClusterIDs == "" {
		return nil, errors.NewBadRequest("cluster_ids is required", nil)
	}
	rawClusterIDCount := strings.Count(rawClusterIDs, ",") + 1
	if rawClusterIDCount > maxSQLClusterStatusIDs {
		return nil, errors.NewBadRequest("too many cluster_ids", nil)
	}

	clusterIDs := make([]uint64, 0, rawClusterIDCount)
	seenClusterIDs := make(map[uint64]struct{}, rawClusterIDCount)
	for _, rawClusterID := range strings.Split(rawClusterIDs, ",") {
		clusterID, err := strconv.ParseUint(rawClusterID, 10, 64)
		if err != nil || clusterID == 0 {
			return nil, errors.NewBadRequest("cluster_id is illegal", err)
		}
		if _, seen := seenClusterIDs[clusterID]; seen {
			continue
		}
		seenClusterIDs[clusterID] = struct{}{}
		clusterIDs = append(clusterIDs, clusterID)
	}
	return clusterIDs, nil
}

func CheckDedicatedClusterPermission(c *gin.Context) {

	var (
		orgId     uint64
		clusterId uint64
		ctx       = c.Request.Context()
	)

	userEmail, err := getUserEmail(c)
	if err != nil {
		tilog.Error(c.Request.Context(), "get user email failed", zap.Error(err))
	}

	orgIdStr := c.Param("org_id")
	orgId, err = strconv.ParseUint(orgIdStr, 10, 64)
	if err != nil {
		abort(c, errors.NewBadRequest("Invalid org_id", err))
		return
	}

	clusterIdStr := c.Param("cluster_id")
	if clusterIdStr != "" {
		clusterId, err = strconv.ParseUint(clusterIdStr, 10, 64)
		if err != nil {
			abort(c, errors.NewBadRequest("Invalid cluster_id", err))
			return
		}
	}

	if clusterId > 0 {
		clusterInfo, err := grpc.GetClusterById(ctx, clusterId)
		if err != nil {
			abort(c, err)
			return
		}
		if service.IsDedicatedCluster(clusterInfo) {
			if !service.IsAWSDedicatedCluster(clusterInfo) {
				abort(c, errors.NewBadRequest("the cluster does not support chat2query", err))
				return
			}
			dedicatedAllowed, err := grpc.IsOrgHitDedicatedWhitelist(ctx, orgId, userEmail)
			if err != nil {
				abort(c, err)
				return
			} else if !dedicatedAllowed {
				abort(c, errors.NewBadRequest("user no permissions to access chat2query", nil))
				return
			}
		}
	}
	c.Next()
}

func getUserEmail(c *gin.Context) (string, error) {
	user, ok := c.Get(consts.CONTEXT_CURRENT_USER)
	if !ok {
		return "", errors.NewUnauthorized("user not found", nil)
	}
	createdBy := user.(*dto.User).Email
	return createdBy, nil
}

func CheckRequestBodySize(c *gin.Context) {
	ctx := c.Request.Context()
	x := c.Request.ContentLength
	if x > MAX {
		tilog.Error(ctx, "ERROR:CheckRequestBodySize exceed max size!")
		c.JSON(http.StatusForbidden, &dto.BasicRes{
			Code:    http.StatusForbidden,
			Message: fmt.Sprintf("%s", errs.ErrInvalidSize),
		})
		c.Abort()
		return
	}
	c.Next()
}

func CheckBotRequestBodySize(c *gin.Context) {
	x := c.Request.ContentLength
	if x > BOT_MAX {
		tigin.AbortWithError(c, errors.NewBadRequest("request body is too large", nil))
		return
	}
	c.Next()
}

func parsePath(path string) string {
	itemList := strings.Split(path, "/")
	if len(path) >= 11 {
		return strings.Join(itemList[11:], "/")
	}
	return path
}

// Metric middleware
func Metric() gin.HandlerFunc {
	return func(c *gin.Context) {
		tBegin := time.Now()
		c.Next()

		duration := float64(time.Since(tBegin)) / float64(time.Second)

		path := c.FullPath()

		metrics.HTTPReqTotal.With(prometheus.Labels{
			"method": c.Request.Method,
			"path":   path,
			"status": strconv.Itoa(c.Writer.Status()),
		}).Inc()

		metrics.HTTPReqDuration.With(prometheus.Labels{
			"method": c.Request.Method,
			"path":   path,
		}).Observe(duration)
	}
}

func authorization(c *gin.Context) (*dto.User, bool) {
	ctx := c.Request.Context()
	user, bizErr := getUser(c, ctx)
	if bizErr != nil {
		logger.LogWithError(ctx, bizErr, "ERROR:rpc/account-provider GetUserByToken failed!")
		authFailRes(c, bizErr)
		return nil, true
	}
	listOrgsByUserReq := &account.ListOrgsByUserRequest{UserId: user.Id}

	orgFlag := false
	orgid := c.Param("org_id")
	orgidInt, err := strconv.ParseUint(orgid, 10, 64)
	if err != nil {
		logger.Warn(ctx, fmt.Sprintf("ERROR:authorization orgid parse failed! user:%s,err:%v", user, err))
		c.Error(errors.NewBadRequest("Invalid org_id", err))
		return nil, true
	}
	orgs, bizErr := grpc.ListOrgsByUser(ctx, listOrgsByUserReq)
	if bizErr != nil {
		logger.LogWithError(ctx, bizErr, fmt.Sprintf("ERROR:rpc/account-provider ListOrgsByUser internal failed! user:%s,err:%v", user, bizErr))
		c.Error(bizErr)
		return nil, true
	}
	for _, v := range orgs.Orgs {
		if v.Id == orgidInt {
			orgFlag = true
			break
		}
	}
	if !orgFlag {
		logger.Warn(ctx, fmt.Sprintf("ERROR:rpc/account-provider ListOrgsByUser check failed! user:%s", user))
		authForbidden(c, errs.ErrUnauthorizedOrg)
		return nil, true
	}

	projectid := c.Param("project_id")
	_, err = strconv.ParseUint(projectid, 10, 64)
	if err != nil {
		logger.Warn(ctx, fmt.Sprintf("ERROR:authorization project_id parse failed! user:%s,err:%v", user, err))
		c.Error(errors.NewBadRequest("Invalid project_id", err))
		return nil, true
	}
	/*
		role, bizErr := grpc.GetAuthUser(ctx, &account.GetAuthUserRequest{UserEmail: user.Email, OrgId: orgidInt})
		if bizErr != nil {
			tilog.Error(ctx, fmt.Sprintf("ERROR:rpc/account-provider GetAuthUser internal failed! user:%s,err:%v", user, bizErr))
			c.Error(bizErr)
			return nil, true
		}
		roleId := role.OrgRole.Number()
		if roleId == acl.OrgRole_ORG_ROLE_AUDIT_ADMIN.Number() || roleId == acl.OrgRole_ORG_ROLE_BILLING_ADMIN.Number() {
			tilog.Error(ctx, fmt.Sprintf("ERROR:rpc/account-provider GetAuthUser check failed! user type:%d,user:%s", roleId, user))
			authForbidden(c, errs.ErrUnauthorizedRole)
			return nil, true
		}
	*/
	currentUser := &dto.User{
		User: user,
	}
	return currentUser, false
}

func getUser(c *gin.Context, ctx context.Context) (*account.User, error) {
	rawToken, err := GetToken(c)
	if err != nil {
		return nil, err
	}
	req := &account.GetUserByTokenRequest{
		Token: rawToken,
	}
	resp, bizErr := grpc.GetUserByToken(ctx, req)
	if bizErr != nil {
		return nil, bizErr
	}
	return resp.User, nil
}

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 a 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 "", errors.NewUnauthorized("Token not found", nil)
	}

	tokenSlice := strings.Split(header, " ")
	if len(tokenSlice) != 2 || strings.ToLower(tokenSlice[0]) != "bearer" {
		return "", errors.NewUnauthorized("Invalid token", nil)
	}

	return tokenSlice[1], nil
}

func authFailRes(c *gin.Context, err error) {
	c.JSON(http.StatusUnauthorized, &dto.BasicRes{
		Code:    http.StatusUnauthorized,
		Message: fmt.Sprintf("%s", err),
	})
}

func authForbidden(c *gin.Context, err error) {
	c.JSON(http.StatusForbidden, &dto.BasicRes{
		Code:    http.StatusForbidden,
		Message: fmt.Sprintf("%s", err),
	})
}

func testGetToken(c *gin.Context) (*dto.User, bool) {
	// header := c.GetHeader("test")
	// if header != "" {
	// 	currentUser := &dto.User{
	// 		User: &account.User{Email: header},
	// 	}
	// 	return currentUser, true
	// }
	return nil, false
}

func abort(c *gin.Context, err error) {
	c.Error(err)
	c.Abort()
}

var logParamFields = []string{"org_id", "project_id", "cluster_id", "session_id"}

func InjectLogger() gin.HandlerFunc {
	return func(gctx *gin.Context) {
		ctx := gctx.Request.Context()
		logger := tilog.WithContextL(ctx)
		fields := make([]zap.Field, 0, len(logParamFields))
		for _, param := range logParamFields {
			if value := gctx.Param(param); value != "" {
				fields = append(fields, zap.String(param, value))
			}
		}
		ctx = contextvar.WithLogger(ctx, logger.With(fields...))
		gctx.Request = gctx.Request.WithContext(ctx)
		gctx.Next()
	}
}
