package controller

import (
	"dataservices/commons/constants"
	errs1 "dataservices/commons/errs"
	"dataservices/commons/ginparser"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	service "dataservices/internal/service/central"
	"net/http"
	"strings"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/gin-gonic/gin"
)

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

	ableToCreateApiKeyRoleMapper = map[string]bool{
		constants.Chat2QueryRoleAdmin:              true,
		constants.Chat2QueryRoleDataContextManager: true,
		//constants.Chat2QuerySQLExecutorReadOnly:    true,
		constants.Chat2QuerySQLExecutorReadWrite: true,
		constants.ApikeyRoleOwner:                true,
	}
)

// List API Keys godoc
// @Summary list api keys
// @Description list api keys
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param source query string true "app key source, multiple split by commas"
// @Accept json
// @Produce json
// @Success 200 {object} dto.ListAPIKeysRes
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys [get]
func ListApiKeys(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[get]-ListApiKeys param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}

	tilog.Info(ctx, "ListApiKeys payload", zap.String("user", createdBy))
	resp, err := service.RbacListApiKeys(ctx, params.AppID, true)
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[get]-ListApiKeys execution failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	resJson := &dto.ListAPIKeysRes{
		BasicRes: dto.DefaultSuccessBasicRes(),
		Data:     resp,
	}
	c.JSON(http.StatusOK, resJson)
}

// Create API Key godoc
// @Summary create api key
// @Description create api key
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param request_body body entity.CreateAPIKeyParam true "request_body"
// @Accept json
// @Produce json
// @Success 200 {object} dto.CreateAPIKeyRes
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys [post]
func CreateApiKey(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}
	input := entity.CreateAPIKeyParam{}
	err := params.BindParam(&input)
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey param bind failed!", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	tilog.Info(ctx, "CreateApiKey payload", zap.Any("input", input), zap.String("user", createdBy))
	if validApiKeyRoleMapper[input.Role] == false {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey invalid role!", zap.Error(err), zap.String("user", createdBy))
		c.Error(errs.NewBadRequest("invalid role", nil))
		return
	}

	if ableToCreateApiKeyRoleMapper[input.Role] {
		// chat2query app: for all the roles we create, user must have permission to create app key
		// dataapp: if user want to create a app key with owner role, user must have permission to create app key; but everyone can create a readonly key(so no check is required)
		userId, _ := params.GetUserId()
		hasPermission, err := service.CheckUserHasCreateWriteKeyPermission(ctx, userId, params.OrgID, params.ProjectID)
		if err != nil {
			tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey CheckUserHasCreateWriteKeyPermission failed!", zap.Error(err), zap.String("user", createdBy))
			c.Error(err)
			return
		}
		if !hasPermission {
			tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey User has no permission to create a read-write apikey.", zap.String("user", createdBy))
			c.Error(errs.NewForbidden("User has no permission to create a read-write apikey.", nil))
			return
		}
	}

	resp, err := service.RbacCreateApikey(ctx, params.AppID, input.Name, input.Role, entity.DataAPISourceDataAPIConsole, createdBy, input.RateLimitRpm, account.OperatorType_OPERATOR_TYPE_USER, input.TTLInMin)
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[post]-CreateApiKey execution failed", zap.Error(err), zap.String("user", createdBy))
		if errs.IsError(err, errs1.ErrExceedSize) {
			ginparser.FailRes(c, err)
		} else {
			c.Error(err)
		}
		return
	}
	resJson := &dto.CreateAPIKeyRes{
		BasicRes: dto.DefaultSuccessBasicRes(),
		Data:     resp,
	}
	c.JSON(http.StatusOK, resJson)
}

// Update API Key godoc
// @Summary update api key
// @Description update api key
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param request_body body entity.UpdateAPIKeyParam true "request_body"
// @Accept json
// @Produce json
// @Success 200 {object} dto.GenericSuccessApiResponse
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/{apikey_id} [put]
func UpdateApiKey(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID, ginparser.PARAM_APIKEY_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[put]-UpdateApiKey param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}
	input := entity.UpdateAPIKeyParam{}
	err := params.BindParam(&input)
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[put]-UpdateApiKey param bind failed!", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	if input.OldRole == "" {
		input.OldRole = constants.ApikeyRoleOwner
	}
	if input.NewRole == "" {
		input.NewRole = constants.ApikeyRoleOwner
	}
	tilog.Info(ctx, "UpdateApiKey payload", zap.Any("input", input), zap.String("user", createdBy))
	// check if key already expired
	if int(input.NewTTLInMin) != input.OldTTLInMin {
		key, err := service.RbacGetApiKey(ctx, params.AppID, params.APIKeyID)
		if err != nil {
			tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[put]-UpdateApiKey get key failed", zap.Error(err), zap.String("user", createdBy))
			c.Error(err)
			return
		}
		if key.IsExpired() {
			err = errs1.ErrAPIKeyExpired
			tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[put]-UpdateApiKey key expired", zap.Error(err), zap.String("user", createdBy))
			c.Error(err)
			return
		}
	}
	err = service.RbacUpdateApikey(ctx, params.AppID, params.APIKeyID, input.Name, input.OldRole, input.NewRole, createdBy, input.RateLimitRpm, account.OperatorType_OPERATOR_TYPE_USER, input.OldTTLInMin, int(input.NewTTLInMin))
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[put]-UpdateApiKey execution failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	c.JSON(http.StatusOK, dto.Success())
}

// Delete API Key godoc
// @Summary delete api key
// @Description delete api key
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param apikey_id path string true "apikey ID"
// @Param role query string true "current role"
// @Accept json
// @Produce json
// @Success 200 {object} dto.GenericSuccessApiResponse
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/{apikey_id} [delete]
func DeleteApiKey(c *gin.Context) {
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID, ginparser.PARAM_APIKEY_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[delete]-DeleteApiKey param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}

	role := strings.TrimSpace(c.Query("role"))
	if role == "" {
		role = constants.ApikeyRoleOwner
	}
	err := service.RbacDeleteApikey(ctx, params.AppID, params.APIKeyID, role, createdBy, account.OperatorType_OPERATOR_TYPE_USER)
	if err != nil {
		tilog.Error(ctx, "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys[delete]-DeleteApiKey execution failed!", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	c.JSON(http.StatusOK, dto.Success())
}

const CONST_expire_key_ttl_val = -525600 //525600 minutes, equals to 1 year, used to expire a key

// Expire API Key godoc
// @Summary expire assigned api key
// @Description expire api key
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Param apikey_id path string true "apikey ID"
// @Accept json
// @Produce json
// @Success 200 {object} dto.GenericSuccessApiResponse
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/{apikey_id}/expire_key [post]
func ExpireApiKey(c *gin.Context) {
	logPrefix := "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/{apikey_id}/expire_key[put]-ExpireApiKey "
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID, ginparser.PARAM_APIKEY_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, logPrefix+"param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}

	key, err := service.RbacGetApiKey(ctx, params.AppID, params.APIKeyID)
	if err != nil {
		tilog.Error(ctx, logPrefix+"get key failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	if key.IsExpired() {
		c.JSON(http.StatusOK, dto.Success())
		return
	}

	err = service.RbacUpdateApikey(ctx, params.AppID, params.APIKeyID, key.Name, key.Role, key.Role, createdBy, key.RateLimitRpm, account.OperatorType_OPERATOR_TYPE_USER, key.TTLInMin, CONST_expire_key_ttl_val)
	if err != nil {
		tilog.Error(ctx, logPrefix+"execution failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	c.JSON(http.StatusOK, dto.Success())
}

// Expire All API Key godoc
// @Summary expire all api keys
// @Description expire all api keys
// @Tags dataapi
// @Param org_id   path string true "org ID"
// @Param project_id path string true "project ID"
// @Param app_id path string true "app ID"
// @Accept json
// @Produce json
// @Success 200 {object} dto.GenericSuccessApiResponse
// @Router /orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/expire_all [post]
func ExpireAllApiKeys(c *gin.Context) {
	logPrefix := "DS-Central-/orgs/{org_id}/projects/{project_id}/apps/{app_id}/apikeys/expire_all[post]-ExpireAllApiKeys "
	ctx := c.Request.Context()
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID, ginparser.PARAM_APP_ID}
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		tilog.Error(ctx, logPrefix+"param validate failed!", zap.Error(validateErr), zap.String("user", createdBy))
		c.Error(validateErr)
		return
	}

	keyList, err := service.RbacListApiKeys(ctx, params.AppID, true)
	if err != nil {
		tilog.Error(ctx, logPrefix+"get key list failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	errG, errCtx := errgroup.WithContext(ctx)
	errG.SetLimit(10)
	for _, key := range keyList {
		if key.IsExpired() {
			continue
		}
		localkey := key
		errG.Go(func() error {
			err := service.RbacUpdateApikey(errCtx, params.AppID, localkey.Id, localkey.Name, localkey.Role, localkey.Role, createdBy, localkey.RateLimitRpm, account.OperatorType_OPERATOR_TYPE_USER, localkey.TTLInMin, CONST_expire_key_ttl_val)
			if err != nil {
				tilog.Error(ctx, logPrefix+"RbacUpdateApikey failed", zap.Error(err), zap.String("user", createdBy))
			}
			return err
		})
	}
	if err := errG.Wait(); err != nil {
		tilog.Error(ctx, logPrefix+"execution failed", zap.Error(err), zap.String("user", createdBy))
		c.Error(err)
		return
	}
	c.JSON(http.StatusOK, dto.Success())
}
