// Package service add by blacknc <blacknc@163.com> at 2023/02/20 16:37:36
package service

import (
	"context"
	"dataservices/commons/constants"
	errs1 "dataservices/commons/errs"
	"dataservices/commons/grpc"
	"dataservices/commons/metrics"
	"dataservices/commons/utils"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/viper"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/errs"
	"google.golang.org/protobuf/types/known/timestamppb"
)

var validRole = map[string]bool{
	constants.ApikeyRoleOwner:                  true,
	constants.ApikeyRoleReadonly:               true,
	constants.Chat2QueryRoleAdmin:              true,
	constants.Chat2QueryRoleDataContextManager: true,
	constants.Chat2QuerySQLExecutorReadWrite:   true,
	constants.Chat2QuerySQLExecutorReadOnly:    true,
}

// const val, a time means never expired here
// we choose this because it will finally stored in mysql using `timestamp` type,
// which is actually int32 and 0x7fffffff is the maximum num
// alarm: this will cause problem after 2037-01-19
var endOfTime = time.Unix(0x7fffffff, 0)

func CheckUserHasCreateWriteKeyPermission(ctx context.Context, userId uint64, orgId uint64, projectId uint64) (bool, error) {
	resourceUri := "orgs/" + strconv.FormatUint(orgId, 10) + "/projects/" + strconv.FormatUint(projectId, 10) + "/dataservices/central/create_readwrite_apikey"
	rbacRequest := account.RbacPermissionRequest{UserId: userId, ResourceUri: resourceUri}
	rbacResponse, err := grpc.HasPermissionForUser(ctx, &rbacRequest)
	if err != nil {
		return false, err
	}
	return rbacResponse.AuthResult, nil
}

func validateRateLimitRpm(orgID uint64, rateLimitRpm int) error {
	maxRPM := GetOrgApikeyRateLimit(orgID)
	if rateLimitRpm <= 0 || rateLimitRpm > maxRPM {
		return errs.NewBadRequest(fmt.Sprintf("rate_limit_rpm must be in the range of 1 and %d", maxRPM), nil)
	}
	return nil
}

func RbacCreateApikey(ctx context.Context, appId uint64, description, role, source, createdBy string, rateLimitRpm int, operatorType account.OperatorType, ttlInMin uint32) (result *dto.RbacApiKey, err error) {

	defer func() { recordCounterMetric(metrics.ApikeyCreateCounter, err) }()

	dataapp, err := repository.GetDataApp(appId)
	if err != nil {
		return nil, errs.NewInternalDB("get data app failed", err)
	}
	if dataapp.AppType == entity.DataAppTypeChat2Query {
		rateLimitRpm = 100
	}

	if err := validateRateLimitRpm(dataapp.OrgId, rateLimitRpm); err != nil {
		return nil, err
	}

	//limit
	apikeysList, err := grpc.GetApiKeysByResource(ctx, &account.GetApiKeysByResourceReq{
		ScopeType:  entity.ScopeTypeDATAAPP,
		ResourceId: appId,
	})
	if err != nil {
		return nil, err
	}
	if len(apikeysList.ApiKeys) >= APIKEYSLIMIT {
		return nil, errs.NewError(errs1.ErrExceedSize, fmt.Sprintf("apikeys count exceed limit "+strconv.Itoa(APIKEYSLIMIT)), nil)
	}

	if !validRole[role] {
		return nil, errs.NewBadRequest("Unsupported apikey role: "+role, nil)
	}
	var shouldExpire *timestamppb.Timestamp
	if ttlInMin > 0 {
		shouldExpire = timestamppb.New(time.Now().Add(time.Duration(ttlInMin) * time.Minute))
	}
	var apikeyInfo *account.CreateApiKeyRsp
	err = repository.Transaction(ctx, func(ctx context.Context) error {
		req := account.CreateApiKeyReq{
			Source:       source,
			Description:  description,
			ScopeType:    entity.ScopeTypeDATAAPP,
			ResourceIds:  []uint64{appId},
			Operator:     createdBy,
			Role:         role,
			RateLimitRpm: uint32(rateLimitRpm),
			OperatorType: &operatorType,
			ExpiredAt:    shouldExpire,
		}
		// create apikey
		apikeyInfo, err = grpc.CreateApiKey(ctx, &req)
		if err != nil {
			return err
		}

		// publish apikey event
		return publishApiKeyEvent(ctx, appId, apikeyInfo.ApiKey.Id, createdBy, dto.DataAppBindClusterActionAddApiKey)
	})
	if err != nil {
		return nil, err
	}

	//encode := utils.Base64Encode(apikeyInfo.ApiKey.AccessKey + ":" + apikeyInfo.ApiKey.SecretKey)
	result = &dto.RbacApiKey{
		Id:        apikeyInfo.ApiKey.Id,
		AccessKey: apikeyInfo.ApiKey.AccessKey,
		SecretKey: apikeyInfo.ApiKey.SecretKey,
		//Base64Key:    encode,
		Source:       apikeyInfo.ApiKey.Source,
		Name:         apikeyInfo.ApiKey.Description,
		CreatedBy:    apikeyInfo.ApiKey.CreatedBy,
		UpdatedBy:    apikeyInfo.ApiKey.UpdatedBy,
		CreatedAt:    utils.JsonTime{Time: apikeyInfo.ApiKey.CreatedAt.AsTime()},
		UpdatedAt:    utils.JsonTime{Time: apikeyInfo.ApiKey.UpdatedAt.AsTime()},
		Role:         role,
		RateLimitRpm: rateLimitRpm,
	}
	result.SetExpireInfo(shouldExpire)
	return result, nil
}

func RbacUpdateApikey(ctx context.Context, dataappID, apikeyID uint64, description, oldRole, newRole, createdBy string, rateLimitRpm int, operatorType account.OperatorType, oldTTL, newTTL int) (err error) {

	defer recordCounterMetric(metrics.ApikeyUpdateCounter, err)

	dataapp, err := repository.GetDataApp(dataappID)
	if err != nil {
		return errs.NewInternalDB("get data app failed", err)
	}
	if dataapp.AppType == entity.DataAppTypeChat2Query {
		rateLimitRpm = 100
	}

	if err := validateRateLimitRpm(dataapp.OrgId, rateLimitRpm); err != nil {
		return err
	}

	if !validRole[oldRole] {
		return errs.NewBadRequest("Unsupported apikey role: "+oldRole, nil)
	}
	if !validRole[newRole] {
		return errs.NewBadRequest("Unsupported apikey role: "+newRole, nil)
	}

	var expireAt *timestamppb.Timestamp
	if oldTTL != newTTL {
		if newTTL == 0 { // set to never expire
			expireAt = timestamppb.New(endOfTime)
		} else {
			expireAt = timestamppb.New(time.Now().Add(time.Duration(newTTL) * time.Minute))
		}
	} //else: no update

	_, err = grpc.UpdateApiKey(ctx, &account.UpdateApiKeyReq{
		ApiKeyId:     apikeyID,
		Description:  description,
		ResourceId:   dataappID,
		ScopeType:    entity.ScopeTypeDATAAPP,
		Operator:     createdBy,
		OriginRole:   oldRole,
		NewRole:      newRole,
		RateLimitRpm: uint32(rateLimitRpm),
		OperatorType: &operatorType,
		ExpiredAt:    expireAt,
	})
	return err
}

func RbacDeleteApikey(ctx context.Context, appID, apiKeyID uint64, role string, createdBy string, operatorType account.OperatorType) (err error) {

	defer recordCounterMetric(metrics.ApikeyDeleteCounter, err)

	// recover apikey permission
	_, err = grpc.RecoverApiKeyPermission(ctx, &account.RecoverApiKeyPermissionReq{
		ApiKeyId:     apiKeyID,
		ScopeType:    entity.ScopeTypeDATAAPP,
		ResourceId:   appID,
		Operator:     createdBy,
		Role:         role,
		OperatorType: &operatorType,
	})
	if err != nil {
		return err
	}

	// publish apikey event
	publishApiKeyEvent(ctx, appID, apiKeyID, createdBy, dto.DataAppBindClusterActionDelApiKey)

	return nil
}

func RbacListApiKeys(ctx context.Context, appID uint64, isDes bool) ([]dto.RbacApiKey, error) {
	apikeysList, err := grpc.GetApiKeysByResource(ctx, &account.GetApiKeysByResourceReq{
		ScopeType:  entity.ScopeTypeDATAAPP,
		ResourceId: appID,
	})
	if err != nil {
		return nil, err
	}

	result := make([]dto.RbacApiKey, 0, len(apikeysList.ApiKeys))
	for _, apiKey := range apikeysList.ApiKeys {
		sk := apiKey.SecretKey
		if isDes {
			sk = skDesensitize(sk)
		}
		var role string
		if len(apiKey.ResourceInfos) > 0 {
			role = apiKey.ResourceInfos[0].Role
		}
		rateLimitRpm := apiKey.RateLimitRpm
		if rateLimitRpm <= 0 {
			rateLimitRpm = constants.ApikeyDefaultRateLimitRpm
		}
		outputKey := dto.RbacApiKey{
			Id:           apiKey.Id,
			AccessKey:    apiKey.AccessKey,
			SecretKey:    sk,
			Source:       apiKey.Source,
			Name:         apiKey.Description,
			CreatedBy:    apiKey.CreatedBy,
			UpdatedBy:    apiKey.UpdatedBy,
			CreatedAt:    utils.JsonTime{Time: apiKey.CreatedAt.AsTime()},
			UpdatedAt:    utils.JsonTime{Time: apiKey.UpdatedAt.AsTime()},
			Role:         role,
			RateLimitRpm: int(rateLimitRpm),
		}
		if apiKey.ExpiredAt == nil ||
			apiKey.ExpiredAt.AsTime().Equal(endOfTime) ||
			apiKey.ExpiredAt.AsTime().After(endOfTime) {
			//never expire
			outputKey.SetExpireInfo(nil)
		} else {
			outputKey.SetExpireInfo(apiKey.ExpiredAt)
		}
		result = append(result, outputKey)
	}
	return result, nil
}

func skDesensitize(sk string) string {
	n := len(sk)
	if n <= 4 {
		return "****"
	}
	return strings.Repeat("*", n-4) + sk[n-4:]
}

func RbacGetApiKey(ctx context.Context, appID uint64, apiKeyID uint64) (*dto.RbacApiKey, error) {
	rbacApiKeys, err := RbacListApiKeys(ctx, appID, true)
	if err != nil {
		return nil, err
	}
	for _, rbacApiKey := range rbacApiKeys {
		if apiKeyID == rbacApiKey.Id {
			return &rbacApiKey, nil
		}
	}
	return nil, errs.NewNotFound("apiKey not found", nil)
}

func GetOrgApikeyRateLimit(orgID uint64) int {
	key := fmt.Sprintf("apiKeyRateLimit.org.%d", orgID)
	if !viper.IsSet(key) {
		return constants.ApikeyDefaultRPM
	}
	return viper.GetInt(key)
}
