// Package service add by blacknc <blacknc@163.com> at 2024/05/24 11:38:55
package service

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/logger"
	"dataservices/commons/metrics"
	comsvc "dataservices/internal/service/common"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"go.uber.org/zap"
)

func CheckApiKeyPermission(ctx context.Context, dataappId, clusterId uint64, rwMode, publicKey string) (err error) {

	var (
		resourceUri   string
		permission    string
		hasPermission bool
		rbacMessage   string
		start         = time.Now()
	)
	metrics.DDBCacheRequestCounter.WithLabelValues("check_apikey_permission", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.CheckApiKeyPermissionDuration.Observe(cost.Seconds())
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "check_apikey").Observe(cost.Seconds())
		logger.LogWithError(ctx, err, "checkApiKeyPermission completed",
			zap.Uint64("dataapp_id", dataappId),
			zap.Uint64("cluster_id", clusterId),
			zap.String("rw_mode", rwMode),
			zap.String("publick_key", publicKey),
			zap.Bool("has_permission", hasPermission),
			zap.String("rbac_message", rbacMessage),
			zap.String("resource_uri", resourceUri),
			zap.Duration("cost", cost),
		)
	}()

	if rwMode == constants.SqlModeReadWrite {
		resourceUri = constants.ApikeyResourceRW
		permission = comsvc.ApikeyModeRW
	} else {
		resourceUri = constants.ApikeyResourceRO
		permission = comsvc.ApikeyModeRO
	}

	defer func() {
		if err != nil || hasPermission {
			return
		}
		metrics.ExecEndpointFailedCounter.WithLabelValues("no_permission").Inc()
		var errMessage string
		if permission == comsvc.ApikeyModeRO {
			errMessage = "Permission denied to execute DQL statement, please try to use the API key with proper permission"
		} else {
			errMessage = "Permission denied to execute DDL/DML statement, please try to use the API key with proper permission"
		}
		err = errs.NewForbidden(errMessage, nil)
	}()

	cacheManager := comsvc.NewCacheManager()
	hasPermission, gerr := cacheManager.GetApikeyPermission(ctx, permission, publicKey, dataappId)
	if gerr == nil {
		return
	} else if gerr != comsvc.ErrNil {
		logger.Error(ctx, "failed to get apikey permission cache", zap.Uint64("dataapp_id", dataappId), zap.String("publickey", publicKey), zap.String("permission", permission), zap.Error(err))
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("check_apikey_permission", "loadddb").Inc()
	result, err := sfGroup.Do(fmt.Sprintf("checkapikeypermission_%d_%s_%s", dataappId, publicKey, permission), func() (any, error) {
		resourceParam := map[string]string{
			"app_id":     strconv.FormatUint(dataappId, 10),
			"cluster_id": strconv.FormatUint(clusterId, 10),
		}
		var (
			hasPermission bool
			rbacMessage   string
			err           error
		)
		if config.IsCentralMode() {
			hasPermission, rbacMessage, err = useadapter.CheckApiKeyPermission(ctx, publicKey, resourceUri, resourceParam)
		} else {
			hasPermission, rbacMessage, err = useadapter.CheckRegionApiKeyPermission(ctx, publicKey, resourceUri, resourceParam)
		}
		if err != nil && strings.Contains(err.Error(), "there is no roles found in the apiKey for this app") {
			hasPermission = false
			if rbacMessage == "" {
				rbacMessage = err.Error()
			}
			err = nil
		}

		if err != nil {
			metrics.ExecEndpointFailedCounter.WithLabelValues("check_apikey_failed").Inc()
			err = errs.NewInternalServerError("Failed to check request permission", err)
			return nil, err
		}
		if err := cacheManager.SetApikeyPermission(ctx, permission, publicKey, dataappId, hasPermission); err != nil {
			logger.LogWithError(ctx, err, "failed to set apikey permission cache", zap.Uint64("dataapp_id", dataappId), zap.String("publickey", publicKey), zap.Bool("has_permission", hasPermission))
		}
		return map[string]any{
			"has_permission": hasPermission,
			"rbac_message":   rbacMessage,
		}, nil
	})
	if err != nil {
		return err
	}
	hasPermission = result.(map[string]any)["has_permission"].(bool)
	rbacMessage = result.(map[string]any)["rbac_message"].(string)
	return nil
}
