package adminapigrpc

import (
	"context"
	"dataservices/commons/constants"
	"dataservices/commons/ginparser"
	"dataservices/commons/pagetoken"
	adminapiv1beta1 "dataservices/idl/pbgen/proto/v1beta1"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	service "dataservices/internal/service/central"
	"fmt"
	"sort"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"
)

type ApiKeyService struct {
	adminapiv1beta1.UnimplementedApiKeyServiceServer
}

func rbacRole2GrpcRole(role string) adminapiv1beta1.ApiKey_Role {
	switch role {
	case constants.ApikeyRoleOwner:
		return adminapiv1beta1.ApiKey_READ_AND_WRITE
	case constants.ApikeyRoleReadonly:
		return adminapiv1beta1.ApiKey_READ_ONLY
	case constants.Chat2QueryRoleAdmin:
		return adminapiv1beta1.ApiKey_CHAT2QUERY_ADMIN
	case constants.Chat2QueryRoleDataContextManager:
		return adminapiv1beta1.ApiKey_CHAT2QUERY_DATA_SUMMARY_MANAGEMENT
	case constants.Chat2QuerySQLExecutorReadOnly:
		return adminapiv1beta1.ApiKey_CHAT2QUERY_SQL_READ_ONLY
	case constants.Chat2QuerySQLExecutorReadWrite:
		return adminapiv1beta1.ApiKey_CHAT2QUERY_SQL_READ_WRITE
	default:
		return adminapiv1beta1.ApiKey_ROLE_UNSPECIFIED
	}
}

func grpcRole2RbacRole(role adminapiv1beta1.ApiKey_Role) (string, error) {
	switch role {
	case adminapiv1beta1.ApiKey_READ_AND_WRITE:
		return constants.ApikeyRoleOwner, nil
	case adminapiv1beta1.ApiKey_READ_ONLY:
		return constants.ApikeyRoleReadonly, nil
	case adminapiv1beta1.ApiKey_CHAT2QUERY_ADMIN:
		return constants.Chat2QueryRoleAdmin, nil
	case adminapiv1beta1.ApiKey_CHAT2QUERY_DATA_SUMMARY_MANAGEMENT:
		return constants.Chat2QueryRoleDataContextManager, nil
	case adminapiv1beta1.ApiKey_CHAT2QUERY_SQL_READ_ONLY:
		return constants.Chat2QuerySQLExecutorReadOnly, nil
	case adminapiv1beta1.ApiKey_CHAT2QUERY_SQL_READ_WRITE:
		return constants.Chat2QuerySQLExecutorReadWrite, nil
	default:
		return "", errs.NewBadRequest("invalid role parameter", nil)
	}
}

func rbacApiKey2GrpcApiKey(externalAppId string, rbacApiKey *dto.RbacApiKey) *adminapiv1beta1.ApiKey {
	key := &adminapiv1beta1.ApiKey{
		ApiKeyId:     rbacApiKey.Id,
		Name:         fmt.Sprintf("dataApps/%s/apiKeys/%d", externalAppId, rbacApiKey.Id),
		PublicKey:    rbacApiKey.AccessKey,
		PrivateKey:   rbacApiKey.SecretKey,
		Description:  rbacApiKey.Name,
		Role:         rbacRole2GrpcRole(rbacApiKey.Role),
		RateLimitRpm: int32(rbacApiKey.RateLimitRpm),
	}
	switch {
	case rbacApiKey.TTLInMin < 0:
		key.ExpireState = adminapiv1beta1.ApiKey_EXPIRE_STATE_EXPIRED
	case rbacApiKey.TTLInMin == 0:
		key.ExpireState = adminapiv1beta1.ApiKey_EXPIRE_STATE_NEVER_EXPIRE
	case rbacApiKey.TTLInMin > 0:
		key.ExpireState = adminapiv1beta1.ApiKey_EXPIRE_STATE_NOT_EXPIRE
		key.ExpireTime = rbacApiKey.GetExpirePbTs()
	}
	return key
}

func (s *ApiKeyService) ListApiKeys(ctx context.Context, req *adminapiv1beta1.ListApiKeysRequest) (*adminapiv1beta1.ListApiKeysResponse, error) {
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Parent)
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}

	token := pagetoken.PageToken[uint64]{}
	if req.PageToken != "" {
		if err := token.DecodeFromString(req.PageToken); err != nil {
			return nil, errs.NewBadRequest("invalid page token", err)
		}
	}
	pageSize := req.PageSize
	if req.PageSize == 0 {
		pageSize = constants.AdminApiDefaultPageSize
	}

	rbacApiKeys, err := service.RbacListApiKeys(ctx, innerAppId, true)
	if err != nil {
		return nil, err
	}
	sort.Slice(rbacApiKeys, func(i, j int) bool {
		return rbacApiKeys[i].Id < rbacApiKeys[j].Id
	})

	count := 0
	total := len(rbacApiKeys)
	nextToken := pagetoken.PageToken[uint64]{}
	apiKeys := make([]*adminapiv1beta1.ApiKey, 0, req.PageSize)
	for i, rbacApiKey := range rbacApiKeys {
		keyId := rbacApiKey.Id
		if keyId > token.Prev {
			apiKeys = append(apiKeys, rbacApiKey2GrpcApiKey(params.ExternalAppId, &rbacApiKey))
			count++
			if count >= int(pageSize) && i < total-1 {
				nextToken.Prev = keyId
				break
			}
		}
	}
	resp := adminapiv1beta1.ListApiKeysResponse{
		ApiKeys: apiKeys,
	}
	if nextToken.Prev > 0 {
		nextPageToken, err := nextToken.EncodeToString()
		if err != nil {
			tilog.Info(ctx, "next page token encode failed", zap.Error(err))
		} else {
			resp.NextPageToken = nextPageToken
		}
	}
	return &resp, nil
}

func (s *ApiKeyService) CreateApiKey(ctx context.Context, req *adminapiv1beta1.CreateApiKeyRequest) (*adminapiv1beta1.ApiKey, error) {
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Parent)
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	role, err := grpcRole2RbacRole(req.ApiKey.Role)
	if err != nil {
		return nil, err
	}
	_type, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}
	ttl := uint32(0)
	if req.ApiKey.GetApiKeyExpireSetting().GetExpireOption() == adminapiv1beta1.ApiKey_EXPIRE_OPTION_SET_TTL {
		ttl = req.ApiKey.GetApiKeyExpireSetting().GetApiKeyTtl()
		if err := checkTTLParam(ttl); err != nil {
			return nil, err
		}
	}
	var operatorType = account.OperatorType_OPERATOR_TYPE_APIKEY
	if _type == dataapi.CrtType_EMAIL {
		operatorType = account.OperatorType_OPERATOR_TYPE_USER
	}

	res, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}

	if res.AppType == entity.DataAppTypeDataapi {
		if !(role == constants.ApikeyRoleOwner || role == constants.ApikeyRoleReadonly) {
			return nil, errs.NewBadRequest("invalid role parameter", nil)
		}
	} else if res.AppType == entity.DataAppTypeChat2Query {
		if !(role == constants.Chat2QueryRoleAdmin || role == constants.Chat2QueryRoleDataContextManager || role == constants.Chat2QuerySQLExecutorReadOnly || role == constants.Chat2QuerySQLExecutorReadWrite) {
			return nil, errs.NewBadRequest("invalid role parameter", nil)
		}
	} else {
		return nil, errs.NewBadRequest("invalid app type", nil)
	}

	rbacApiKey, err := service.RbacCreateApikey(
		ctx,
		innerAppId,
		req.ApiKey.Description,
		role,
		entity.DataServiceSourceAdminAPI,
		createdBy,
		int(req.ApiKey.RateLimitRpm),
		operatorType,
		ttl,
	)
	if err != nil {
		return nil, err
	}
	return rbacApiKey2GrpcApiKey(params.ExternalAppId, rbacApiKey), nil
}

func (s *ApiKeyService) GetApiKey(ctx context.Context, req *adminapiv1beta1.GetApiKeyRequest) (*adminapiv1beta1.ApiKey, error) {
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Name)
	if params.ApiKeyId <= 0 {
		return nil, errs.NewBadRequest("invalid apiKey resource name", nil)
	}
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	rbacApiKey, err := service.RbacGetApiKey(ctx, innerAppId, params.ApiKeyId)
	if err != nil {
		return nil, err
	}
	return rbacApiKey2GrpcApiKey(params.ExternalAppId, rbacApiKey), nil
}

func (s *ApiKeyService) UpdateApiKey(ctx context.Context, req *adminapiv1beta1.UpdateApiKeyRequest) (*adminapiv1beta1.ApiKey, error) {
	params := ginparser.ParseAdminApiPathParameters(ctx, req.ApiKey.Name)
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	oldRbacApiKey, err := service.RbacGetApiKey(ctx, innerAppId, params.ApiKeyId)
	if err != nil {
		return nil, err
	}
	newRole, err := grpcRole2RbacRole(req.ApiKey.Role)
	if err != nil {
		return nil, err
	}
	_, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}
	if oldRbacApiKey.IsExpired() {
		return nil, errs.NewBadRequest("cannot edit expired key", nil)
	}
	newTTL := 0
	switch req.ApiKey.GetApiKeyExpireSetting().GetExpireOption() {
	case adminapiv1beta1.ApiKey_EXPIRE_OPTION_UNSPECIFIED:
		// keep original setting
		newTTL = oldRbacApiKey.TTLInMin
	case adminapiv1beta1.ApiKey_EXPIRE_OPTION_SET_TTL:
		newTTL = int(req.ApiKey.GetApiKeyExpireSetting().GetApiKeyTtl())
		if err := checkTTLParam(uint32(newTTL)); err != nil {
			return nil, err
		}
	case adminapiv1beta1.ApiKey_EXPIRE_OPTION_NEVER_EXPIRE:
		//do nothing, keep newTTL=0
		break
	}

	err = service.RbacUpdateApikey(
		ctx,
		innerAppId,
		params.ApiKeyId,
		req.ApiKey.Description,
		oldRbacApiKey.Role,
		newRole,
		createdBy,
		int(req.ApiKey.RateLimitRpm),
		account.OperatorType_OPERATOR_TYPE_APIKEY,
		oldRbacApiKey.TTLInMin,
		newTTL,
	)
	if err != nil {
		return nil, err
	}
	newRbacApiKey, err := service.RbacGetApiKey(ctx, innerAppId, params.ApiKeyId)
	if err != nil {
		return nil, err
	}
	return rbacApiKey2GrpcApiKey(params.ExternalAppId, newRbacApiKey), nil
}

func (s *ApiKeyService) DeleteApiKey(ctx context.Context, req *adminapiv1beta1.DeleteApiKeyRequest) (*emptypb.Empty, error) {
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Name)
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	oldRbacApiKey, err := service.RbacGetApiKey(ctx, innerAppId, params.ApiKeyId)
	if err != nil {
		return nil, err
	}
	_type, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}
	var operatorType = account.OperatorType_OPERATOR_TYPE_APIKEY
	if _type == dataapi.CrtType_EMAIL {
		operatorType = account.OperatorType_OPERATOR_TYPE_USER
	}

	err = service.RbacDeleteApikey(ctx, innerAppId, params.ApiKeyId, oldRbacApiKey.Role, createdBy, operatorType)
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func checkTTLParam(ttl uint32) error {
	if ttl == 0 {
		return errs.NewBadRequest("api_key_ttl MUST larger than 0", nil)
	}
	if ttl > 365*24*60 {
		return errs.NewBadRequest("api_key_ttl MUST less or equal to 525600 minutes, which is 1 year", nil)
	}
	return nil
}
