package service

import (
	"context"
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/spf13/viper"

	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/errs"
	"dataservices/commons/grpc"
	"dataservices/commons/http"
	"dataservices/commons/metrics"
	"dataservices/commons/utils"
	dto "dataservices/internal/dto/dataapi"
	dto2 "dataservices/internal/dto/dataapi_gateway"
	"dataservices/internal/entity"
	"dataservices/internal/repository"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	tiErrs "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var appNameRegexp = regexp.MustCompile("^[A-Za-z0-9 _-]*$")

const measureRequestMetricsName = "dataservices_dataapi_executor_measure_request_counter"
const measureNetworkMetricsName = "dataservices_dataapi_executor_measure_network_counter"
const measureEDATokenMetricsName = "eda_token_counter_total"
const measureEDARequestsMetricsName = "eda_request_counter_total"
const measureEDANetworkMetricsName = "eda_network_counter_total"

func GenAppExternalID(appType entity.DataAppType) string {
	var namePrefix string
	if appType == entity.DataAppTypeDataapi {
		namePrefix = "dataapp"
	} else {
		namePrefix = strings.ToLower(string(appType))
	}
	appName := namePrefix + "-" + utils.RandStr(8)
	return appName
}

func ListDataApps(ctx context.Context, projectId uint64, appTypes ...string) ([]*entity.DataApp, error) {
	return repository.ListDataApps(ctx, projectId, appTypes...)
}

func getO11yUrl(ctx context.Context, region string, appInfo *entity.DataApp) string {
	if appInfo.AppType == entity.DataAppTypeChat2Query {
		return getO11yCentralUrl(ctx)
	}

	return getO11yRegionUrl(ctx, region)
}

func getO11yCentralUrl(ctx context.Context) string {
	return viper.GetString(config.O11YCentralUrl + "." + config.CurrentEnv())
}

func getO11yRegionUrl(ctx context.Context, region string) string {
	if config.IsProd() {
		return viper.GetString(config.O11YRegionUrl + "." + region)

	} else {
		url := viper.GetString(config.O11YRegionUrl + "." + config.CurrentEnv())
		if len(url) < 1 {
			tilog.Info(ctx, "config.O11YRegionUrl is nil . ", zap.String("O11YRegionUrl.CurrentEnv", config.O11YRegionUrl+"."+config.CurrentEnv()))
			url = "http://www.ds.us-west-2.aws.observability-staging.pingcap.cloud/internal/metrics/f98fac90-a0d1-4762-a42a-5a13fe650a09/api/v1/query_range"
		}
		return url
	}
}

func getQueryString(ctx context.Context, orgId uint64, inPutParam entity.DataUsageQueryParam, appInfo *entity.DataApp) string {
	urlQueryParams := url.Values{}
	var metricsName string

	if appInfo.AppType == entity.DataAppTypeChat2Query {
		metricsName = measureEDANetworkMetricsName

		if strings.Contains(strings.ToLower(inPutParam.Class), "request") {
			metricsName = measureEDARequestsMetricsName
		} else if strings.Contains(strings.ToLower(inPutParam.Class), "token") {
			metricsName = measureEDATokenMetricsName
		}
	} else {
		metricsName = measureNetworkMetricsName
		if strings.Contains(strings.ToLower(inPutParam.Class), "request") {
			metricsName = measureRequestMetricsName
		}
	}

	if inPutParam.Type == "daily" {
		switch inPutParam.Class {
		case "requestAll", "networkAll", "tokenAll":
			urlQueryParams.Set("query", fmt.Sprintf("(last_over_time(sum(increase(%s{orgId=\"%s\",appId=\"%s\"}[1d] offset -1d))[1d:1d]) or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10)))

		case "byEndpointRequest", "byEndpointNetWork", "byEndpointToken":
			urlQueryParams.Set("query", fmt.Sprintf("(sum by (exported_endpoint) ( last_over_time(increase(%s{orgId=\"%s\",appId=\"%s\"}[1d] offset -1d)[1d:1d]) )  or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10)))
		case "byApiKeyRequest", "byAPIKeyNetWork", "byApiKeyToken":
			urlQueryParams.Set("query", fmt.Sprintf("(sum by (appKey) (last_over_time(increase(%s{orgId=\"%s\",appId=\"%s\"}[1d] offset -1d)[1d:1d]) ) or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10)))

		}
	} else {
		switch inPutParam.Class {
		case "requestAll", "networkAll", "tokenAll":
			urlQueryParams.Set("query", fmt.Sprintf("(sum(rate(%s{orgId=\"%s\",appId=\"%s\"}[30m]))*%s or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10), strconv.Itoa(inPutParam.Step)))

		case "byEndpointRequest", "byEndpointNetWork", "byEndpointToken":
			urlQueryParams.Set("query", fmt.Sprintf("((sum(rate(%s{orgId=\"%s\",appId=\"%s\"}[30m])) by (exported_endpoint)) *%s  or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10), strconv.Itoa(inPutParam.Step)))
		case "byApiKeyRequest", "byAPIKeyNetWork", "byApiKeyToken":
			urlQueryParams.Set("query", fmt.Sprintf("((sum(rate(%s{orgId=\"%s\",appId=\"%s\"}[30m])) by (appKey))*%s  or vector(0))", metricsName, strconv.FormatUint(orgId, 10), strconv.FormatUint(inPutParam.DataAppId, 10), strconv.Itoa(inPutParam.Step)))

		}
	}

	tilog.Info(ctx, "prometheus query expr", zap.String("query", urlQueryParams.Get("query")))

	urlQueryParams.Set("start", strconv.FormatInt(inPutParam.Start, 10))
	urlQueryParams.Set("end", strconv.FormatInt(inPutParam.End, 10))
	urlQueryParams.Set("step", strconv.Itoa(inPutParam.Step))
	return urlQueryParams.Encode()
}

func ListDataAppUsage(ctx context.Context, queryParam entity.DataUsageQueryParam, projectId uint64, orgId uint64) (*entity.O11YResp, error) {
	tilog.Info(ctx, "ListDataAppUsage", zap.Any("queryParam", queryParam), zap.Uint64("projectID", projectId), zap.Uint64("orgID", orgId))

	appInfo, err := GetDataApp(ctx, queryParam.DataAppId)
	if err != nil {
		tilog.Info(ctx, "ListDataAppUsage GetDataApp   error: ", zap.Error(err))
		return nil, err
	}

	tilog.Info(ctx, "ListDataAppUsage", zap.Any("queryParam", queryParam), zap.Uint64("projectID", projectId), zap.Uint64("orgID", orgId), zap.Any("appInfo", appInfo))

	if appInfo.OrgId != orgId || appInfo.ProjectId != projectId {
		return nil, tiErrs.NewForbidden("You are not authorized to access this app", nil)
	}

	if appInfo.CreatedAt.Unix() > queryParam.Start {
		if queryParam.Type == "daily" {
			newStart := time.Date(appInfo.CreatedAt.Year(), appInfo.CreatedAt.Month(), appInfo.CreatedAt.Day(), 0, 0, 0, 0, time.UTC)
			queryParam.Start = newStart.Unix()
		} else {
			queryParam.Start = appInfo.CreatedAt.Unix()
		}
	}

	url := getO11yUrl(ctx, queryParam.Region, appInfo) + "?" + getQueryString(ctx, orgId, queryParam, appInfo)
	tilog.Info(ctx, "ListDataAppUsage query O11Y  : ", zap.String("url", url), zap.Any("queryParam", queryParam), zap.Any("appInfo", appInfo))

	o11YResp, err := http.ListDataAppUsage(ctx, url)
	if err == nil {
		tilog.Info(ctx, "ListDataAppUsage httpclient result: ", zap.Any("resp", o11YResp))

		return o11YResp, nil
	} else {
		tilog.Info(ctx, " ListDataAppUsage httpclient result error: ", zap.Any("resp", o11YResp), zap.Error(err))
		return nil, err
	}

}

func GetDataApp(ctx context.Context, appId uint64) (*entity.DataApp, error) {
	return repository.GetDataApp(appId)
}

func GetDataAppByExtId(ctx context.Context, appExtId string) (*entity.DataApp, error) {
	return repository.GetDataAppByExtId(ctx, appExtId)
}

func CreateDataApp(ctx context.Context, orgId uint64, projectId uint64, createdBy string, in *entity.CreateDataAppParam) (*entity.DataApp, []*entity.Endpoint, error) {
	tilog.Info(ctx, "create data app", zap.Uint64("org_id", orgId), zap.Uint64("project_id", projectId), zap.String("createdBy", createdBy), zap.Any("in", in))

	switch in.AppType {
	case entity.DataAppTypeChat2Query:
		return CreateChat2QueryApp(ctx, orgId, projectId, createdBy, in, entity.DataAppTypeChat2Query)
	case entity.DataAppTypeDataapi:
		return CreateDataapiApp(ctx, orgId, projectId, createdBy, in)
	default:
		return nil, nil, tiErrs.NewBadRequest("Unsupported app_type "+string(in.AppType), nil)
	}
}

func validateAppName(appName string) error {
	if !appNameRegexp.MatchString(appName) {
		return tiErrs.NewBadRequest("App name must be less than 32 characters and can include letters, numbers, hyphens(-), underscores(_) and spaces", nil)
	}
	if len(appName) >= 32 {
		return tiErrs.NewBadRequest("App name must be less than 32 characters and can include letters, numbers, hyphens(-), underscores(_) and spaces", nil)
	}
	return nil
}

func validateAppDescription(description string) error {
	if len([]rune(description)) > 1000 {
		return tiErrs.NewBadRequest("App description maximum length is 1000 characters", nil)
	}
	return nil
}

func validateEndpointDescription(ctx context.Context, description string) error {
	rawDesc, err := url.PathUnescape(description)
	if err != nil {
		tilog.Error(ctx, "endpoint got invalid description", zap.String("desc", description))
		rawDesc = description
	}
	if len([]rune(rawDesc)) > constants.EndpointDescGithubMaxLength {
		return tiErrs.NewBadRequest(fmt.Sprintf("Endpoint description length should not exceed %d characters", constants.EndpointDescGithubMaxLength), nil)
	}
	return nil
}

func validateAppVersion(appVersion string) error {
	parts := strings.Split(appVersion, ".")
	if len(parts) != 3 {
		return tiErrs.NewBadRequest("App version bad length", nil)
	}
	names := []string{"major", "minor", "patch"}
	for i, name := range names {
		intPart, err := strconv.Atoi(parts[i])
		if err != nil {
			return tiErrs.NewBadRequest(fmt.Sprintf("App version %s is not a number", name), err)
		}
		if intPart < 0 || intPart > 9999 {
			return tiErrs.NewBadRequest(fmt.Sprintf("App version %s must be between 0 and 9999", name), nil)
		}
		if i == 0 && intPart == 0 {
			return tiErrs.NewBadRequest(fmt.Sprintf("App version %s must be greater than 0", name), nil)
		}
	}

	return nil
}

func checkCreateDataAppParams(ctx context.Context, orgId uint64, projectId uint64, createdBy string, in *entity.CreateDataAppParam) error {
	var err error

	// check cluster status
	if err := checkClusterStatus(ctx, orgId, projectId, in.ClusterIDs, createdBy); err != nil {
		return err
	}
	// check appName
	if err = validateAppName(in.AppName); err != nil {
		return err
	}
	if err = validateAppDescription(in.Description); err != nil {
		return err
	}
	// give app_version default value
	if in.AppVersion == "" {
		in.AppVersion = "1.0.0"
	}
	if err = validateAppVersion(in.AppVersion); err != nil {
		return err
	}

	return nil
}

func CreateDataapiApp(ctx context.Context, orgId uint64, projectId uint64, createdBy string, in *entity.CreateDataAppParam) (*entity.DataApp, []*entity.Endpoint, error) {
	var (
		dataApp  *entity.DataApp
		endpoint *entity.Endpoint
		err      error
	)

	if err = checkCreateDataAppParams(ctx, orgId, projectId, createdBy, in); err != nil {
		return nil, nil, err
	}

	// check if user has project write permission with sample data app
	if in.IsSample {
		if hasPermis, err := HasProjectRWPermission(ctx, orgId, projectId, in.UserID); err != nil {
			return nil, nil, err
		} else if !hasPermis {
			return nil, nil, errs.NewBadRequest("Unable to create sample data app, no permission to import data", nil)
		}
	}

	var status string
	if in.IsSample {
		status = entity.DataAppStatusInitializing
	} else {
		status = entity.DataAppStatusNormal
	}

	err = repository.Transaction(ctx, func(ctx context.Context) error {
		// create app
		dataApp, err = createRawDataApp(
			ctx,
			orgId,
			projectId,
			entity.DataAppTypeDataapi,
			createdBy,
			in.AppName,
			in.DeploymentRegion,
			in.Description,
			in.AppVersion,
			status,
		)
		if err != nil {
			return err
		}

		//if !in.IsSample {
		//	// create default endpoint
		//	newEndpoint := defaultEndpoint
		//	newEndpoint.DataappId = dataApp.ID
		//	newEndpoint.CreatedBy = createdBy
		//	newEndpoint.UpdatedBy = createdBy
		//	endpoint, err = repository.CreateEndpoint(ctx, &newEndpoint, nil)
		//	if err != nil {
		//		return err
		//	}
		//}

		// create link cluster
		if len(in.ClusterIDs) > 0 {
			if err := createAppClusterWithTx(ctx, dto.CreateAppClusterReq{
				ClusterIDs: in.ClusterIDs,
				DataappID:  dataApp.ID,
				CreatedBy:  createdBy,
			}, true); err != nil {
				return err
			}
		}

		if in.DeploymentConfig.InstallationId != 0 {
			if err = EnableDataAppGithubAutoDeploy(ctx, dataApp.ID, in.DeploymentConfig.PreflightLinkGithubParam, true); err != nil {
				return err
			}
		} else {
			if err = SaveDataAppDeploymentSetting(ctx, dataApp.ID, &entity.DataAppDeploymentConfig{
				DeploymentSwithConfig: entity.DeploymentSwithConfig{
					EnableUIDrafts:   true,
					EnableAutoDeploy: false,
				},
			}); err != nil {
				return err
			}
		}

		return nil
	})

	if !in.IsSample {
		// create deployment task
		if _, err = TriggerDataAppUpdateFromUI(ctx, dataApp.ID, createdBy, "Initial Data App Setup"); err != nil {
			return nil, nil, err
		}
	} else {
		// create sample dataapp task
		if err = CreateSampleDataAppTask(ctx, dataApp.ID, createdBy); err != nil {
			return nil, nil, err
		}
	}

	var endpoints []*entity.Endpoint
	if endpoint != nil {
		endpoints = append(endpoints, endpoint)
	}
	return dataApp, endpoints, err
}

func CreateChat2QueryApp(ctx context.Context, orgId uint64, projectId uint64, createdBy string, in *entity.CreateDataAppParam, appType entity.DataAppType) (*entity.DataApp, []*entity.Endpoint, error) {
	var (
		dataApp   *entity.DataApp
		endpoints []*entity.Endpoint
		err       error
	)

	if err = checkCreateDataAppParams(ctx, orgId, projectId, createdBy, in); err != nil {
		return nil, nil, err
	}

	err = repository.Transaction(ctx, func(ctx context.Context) error {
		dataApp, err = createRawDataApp(ctx, orgId, projectId, appType, createdBy, in.AppName, in.DeploymentRegion, in.Description, "", "")
		if err != nil {
			return err
		}

		version := getReleaseVersion("")
		for _, chatEndpoint := range defaultChatEndpointList {
			tilog.Info(ctx, "check if pingcapOnlyEndpointPath", zap.Any("endpoint", chatEndpoint), zap.Bool("get value", internalOnlyEndpointPath[chatEndpoint.Endpoint]), zap.String("appCreatedBy", dataApp.CreatedBy), zap.String("appExtId", dataApp.ExternalAppID))
			if _, ok := internalOnlyEndpointPath[chatEndpoint.Endpoint]; ok {
				if !IsEDAInternalUser(ctx, dataApp.CreatedBy, dataApp.OrgId) {
					continue
				}
			}

			chatEndpoint.DataappId = dataApp.ID
			chatEndpoint.CreatedBy = createdBy
			chatEndpoint.UpdatedBy = createdBy
			newEndpoint, err := repository.CreateEndpoint(ctx, &chatEndpoint, nil)
			tilog.Info(ctx, "create endpoint", zap.Any("endpoint", chatEndpoint), zap.Any("newEndpoint", newEndpoint), zap.Error(err))
			if err != nil {
				return err
			}
			err = repository.CreateOnlineEndpoint(ctx, newEndpoint, version, createdBy)
			if err != nil {
				return err
			}
			endpoints = append(endpoints, newEndpoint)
		}

		// create link cluster
		if len(in.ClusterIDs) > 0 {
			if err := createAppClusterWithTx(ctx, dto.CreateAppClusterReq{
				ClusterIDs: in.ClusterIDs,
				DataappID:  dataApp.ID,
				CreatedBy:  createdBy,
			}, true); err != nil {
				return err
			}
		}

		// push create dataapp event (sync create dataapp、endpoint、publish endpoint)
		if err := publishDataappEvent(ctx, dataApp.ID, createdBy, dto.DataappActionCreate); err != nil {
			return err
		}

		return nil
	})
	return dataApp, endpoints, err
}

func createRawDataApp(
	ctx context.Context,
	orgId uint64,
	projectId uint64,
	appType entity.DataAppType,
	createdBy,
	appName,
	deployRegion,
	description,
	appVersion string,
	status string,
) (dataApp *entity.DataApp, err error) {

	defer func() {
		ok := strconv.FormatBool(err == nil)
		metrics.CreateDataAppCounter.WithLabelValues(ok).Inc()
	}()

	if deployRegion == "" {
		deployRegion = "Central"
	}
	if appName == "" {
		appName = entity.PredefAppName[appType]
	}
	if appName == "" {
		return nil, errs.ErrNotSupportedAppType
	}
	if status == "" {
		status = entity.DataAppStatusNormal
	}
	dataApp = &entity.DataApp{
		ExternalAppID:    GenAppExternalID(appType),
		AppType:          appType,
		AppName:          appName,
		OrgId:            orgId,
		ProjectId:        projectId,
		DeploymentRegion: deployRegion,
		CreatedBy:        createdBy,
		UpdatedBy:        createdBy,
		Description:      description,
		AppVersion:       appVersion,
		Status:           status,
	}

	return repository.CreateDataApp(ctx, dataApp)
}

func UpdateDataApp(ctx context.Context, appId uint64, input *entity.UpdateDataAppParam, updatedBy string) error {
	if err := CheckDataAppCanDeploy(ctx, appId); err != nil {
		return err
	}
	if err := validateAppName(input.AppName); err != nil {
		return err
	}
	if err := validateAppDescription(input.Description); err != nil {
		return err
	}
	if input.AppVersion != "" {
		if err := validateAppVersion(input.AppVersion); err != nil {
			return err
		}
	}

	appInfo, err := repository.GetDataApp(appId)
	if err != nil {
		return err
	}

	if input.DeploymentRegion == "" || appInfo.AppType == entity.DataAppTypeChat2Query {
		input.DeploymentRegion = "Central"
	}
	dataApp := &entity.DataApp{
		ID:               appId,
		AppName:          input.AppName,
		DeploymentRegion: input.DeploymentRegion,
		UpdatedBy:        updatedBy,
		AppVersion:       input.AppVersion,
		Description:      input.Description,
	}
	return repository.Transaction(ctx, func(ctx context.Context) error {
		if err := repository.UpdateDataApp(ctx, dataApp); err != nil {
			return err
		}
		if _, err := TriggerDataAppUpdateFromUI(ctx, dataApp.ID, updatedBy, ""); err != nil {
			return err
		}
		return nil
	})
}

func DeleteDataApp(ctx context.Context, appId uint64, operator string, operatorType account.OperatorType) error {
	return repository.Transaction(ctx, func(ctx context.Context) error {
		// delete app
		if err := deleteRawDataApp(ctx, appId, operator, operatorType); err != nil {
			return err
		}
		// clear github linkage
		if err := repository.ClearDataAppDeploymentSettingsGithubLikageByDataappId(ctx, appId); err != nil {
			return err
		}
		// push delete dataapp event (sync delete dataapp、endpoint、publish endpoint)
		if err := publishDataappEvent(ctx, appId, operator, dto.DataappActionDelete); err != nil {
			return err
		}
		return nil
	})
}

func deleteRawDataApp(ctx context.Context, appId uint64, operator string, operatorType account.OperatorType) error {

	// list app's all apikey
	apikeysList, err := grpc.GetApiKeysByResource(ctx, &account.GetApiKeysByResourceReq{
		ScopeType:  entity.ScopeTypeDATAAPP,
		ResourceId: appId,
	})
	if err != nil {
		return err
	}

	// Recover app's all apikey permission
	req := &account.RecoverResourceRefAllApiKeyPermissionReq{
		ScopeType:    entity.ScopeTypeDATAAPP,
		ResourceId:   appId,
		Operator:     operator,
		OperatorType: &operatorType,
	}
	if _, err := grpc.RecoverResourceRefAllApiKeyPermission(ctx, req); err != nil {
		tilog.Error(ctx, "DeleteDataApp recover all apikey permission failed", zap.Error(err), zap.Any("req", req))
		metrics.ApikeyRecoverFailedCounter.With(nil).Inc()
	}

	// send delete apikey eventbus message
	for _, apikey := range apikeysList.ApiKeys {
		if perr := publishApiKeyEvent(ctx, appId, apikey.Id, operator, dto.DataAppBindClusterActionDelApiKey); perr != nil {
			metrics.ApikeyRecoverFailedCounter.With(nil).Inc()
		}
	}

	// Delete app and app's endpoint
	if err := repository.DeleteDataApp(ctx, appId); err != nil {
		metrics.DeleteDataAppCounter.With(prometheus.Labels{"ok": "false"}).Inc()
		return err
	}
	metrics.DeleteDataAppCounter.With(prometheus.Labels{"ok": "true"}).Inc()
	return nil
}

func GetAPPServiceUrl(ctx context.Context, appId, clusterID uint64, regionSuccessfulDomainsMap map[string]*entity.CustomDomain) (string, error) {
	var region string
	baseurlErr := ""
	if clusterID > 0 {
		clusterInfo, err := grpc.GetClusterWithoutPermCheck(ctx, clusterID)
		if err != nil {
			tilog.Error(ctx, "GetAPPServiceUrl-GetClusterWithoutPermCheck failed!", zap.Error(err))
			return baseurlErr, nil
		}
		region = clusterInfo.RegionName
	}
	appInfo, err := repository.GetDataApp(appId)
	if err != nil {
		tilog.Error(ctx, "GetAPPServiceUrl-GetDataApp failed!", zap.Error(err))
		return baseurlErr, nil
	}
	return GenAPPServiceUrl(appInfo, region, regionSuccessfulDomainsMap), nil
}

func GenAPPServiceUrl(appInfo *entity.DataApp, region string, regionSuccessfulDomainsMap map[string]*entity.CustomDomain) string {
	if region != "" {
		if !config.IsProd() {
			region = constants.DefaultAWSRegion
		}
		customDomain, ok := regionSuccessfulDomainsMap[region]
		if !ok {
			return fmt.Sprintf(`https://%s.%s%s`, region, getEndpointHost(), GetAppDefaultPath(appInfo))
		} else {
			if customDomain.CustomPath == "/" {
				return fmt.Sprintf(`https://%s`, customDomain.CustomDomainName)
			} else {
				return fmt.Sprintf(`https://%s%s`, customDomain.CustomDomainName, customDomain.CustomPath)
			}
		}
	} else {
		return fmt.Sprintf(`https://%s%s`, getEndpointHost(), GetAppDefaultPath(appInfo))
	}
}

func GetAppDefaultPath(appInfo *entity.DataApp) string {
	return fmt.Sprintf(`/api/v1beta/app/%s/endpoint`, appInfo.ExternalAppID)
}

func RefreshChat2QueryApp(ctx context.Context) error {
	appList, err := repository.ListAllDataApps(ctx, nil, string(entity.DataAppTypeChat2Query))
	if err != nil {
		return err
	}

	version := getReleaseVersion("")

	for _, app := range appList {
		if app.AppType != entity.DataAppTypeChat2Query {
			continue
		}

		// delete all chat2query endpoints first
		endpoints, err := repository.ListEndpoints(ctx, app.ID, entity.EndpointTypeChat2EDA, entity.EndpointTypeChat2Data)
		if err != nil {
			tilog.Error(ctx, "failed to get endpoints", zap.Error(err))
			continue
		}

		hasError := false
		for _, i := range endpoints {
			tilog.Info(ctx, "start to delete endpoint", zap.Any("app", app), zap.Any("endpoint", i))

			err = OfflineEndpoint(ctx, i.ID, i.CreatedBy)
			if err != nil {
				tilog.Error(ctx, "failed to offline endpoint", zap.Any("endpoint", i), zap.Error(err))
				// if we offline multiple times, it may cause error, but we still need to delete it
			}

			err = repository.DeleteEndpoint(ctx, i.ID, i.CreatedBy)
			tilog.Info(ctx, "end to delete endpoint", zap.Any("app", app), zap.Any("endpoint", i), zap.Error(err))
			if err != nil {
				tilog.Error(ctx, "failed to delete endpoint", zap.Any("endpoint", i), zap.Error(err))
				hasError = true
				continue
			}
			if err = publishEndpointEvent(ctx, i.ID, i.CreatedBy, dto.EndpointActionDelete); err != nil {
				tilog.Error(ctx, "failed to publish endpoint event", zap.Any("endpoint", i), zap.Error(err))
				hasError = true
				continue
			}
		}
		if hasError {
			tilog.Error(ctx, "failed to delete all endpoints", zap.Any("app", app))
			continue
		}

		// refresh endpoints for chat2query app
		for _, chatEndpoint := range defaultChatEndpointList {
			chatEndpoint.DataappId = app.ID
			chatEndpoint.CreatedBy = app.CreatedBy
			chatEndpoint.UpdatedBy = app.CreatedBy

			tilog.Info(ctx, "check if pingcapOnlyEndpointPath", zap.Any("endpoint", chatEndpoint), zap.Bool("get value", internalOnlyEndpointPath[chatEndpoint.Endpoint]), zap.String("appCreatedBy", app.CreatedBy), zap.String("appExtId", app.ExternalAppID))
			if _, ok := internalOnlyEndpointPath[chatEndpoint.Endpoint]; ok {
				if !IsEDAInternalUser(ctx, app.CreatedBy, app.OrgId) {
					continue
				}
			}

			newEndpoint, err := repository.CreateEndpoint(ctx, &chatEndpoint, nil)
			tilog.Info(ctx, "create endpoint", zap.Any("endpoint", chatEndpoint), zap.Any("newEndpoint", newEndpoint), zap.Error(err))
			if err != nil {
				tilog.Error(ctx, "failed to create endpoint", zap.Any("endpoint", chatEndpoint), zap.Error(err))
				continue
			}

			err = repository.CreateOnlineEndpoint(ctx, newEndpoint, version, app.CreatedBy)
			if err != nil {
				tilog.Error(ctx, "failed to create online endpoint", zap.Any("endpoint", chatEndpoint), zap.Error(err))
				continue
			}
		}

		// push create dataapp event (sync create dataapp、endpoint、publish endpoint)
		if err := publishDataappEvent(ctx, app.ID, app.CreatedBy, dto.DataappActionCreate); err != nil {
			tilog.Error(ctx, "failed to publish dataapp event", zap.Any("app", app), zap.Error(err))
			continue
		}

		// bind all clusters to the current app ID
		clusterIDs, err := getProjectAllClusterIDs(ctx, app.OrgId, app.ProjectId)
		if err != nil {
			tilog.Error(ctx, "failed to get project all cluster ids", zap.Error(err))
			continue
		}

		bindingClusterIDs := []uint64{}
		for _, clusterID := range clusterIDs {
			if err = checkClusterStatus(ctx, app.OrgId, app.ProjectId, []uint64{clusterID}, app.CreatedBy); err == nil {
				bindingClusterIDs = append(bindingClusterIDs, clusterID)
			} else {
				tilog.Error(ctx, "failed to check cluster status", zap.Error(err), zap.Any("cluster_id", clusterID))
			}
		}

		req := dto.CreateAppClusterReq{
			ClusterIDs: bindingClusterIDs,
			DataappID:  app.ID,
			CreatedBy:  app.CreatedBy,
			OrgID:      app.OrgId,
			ProjectID:  app.ProjectId,
		}
		if err := createAppClusterWithTx(ctx, req, true); err != nil {
			tilog.Error(ctx, "failed to create app cluster", zap.Error(err), zap.Any("req", req))
			continue
		}
	}

	return nil
}

func RefreshDataapiAPP(ctx context.Context, appIDs []uint64) error {
	appList, err := repository.ListAllDataApps(ctx, appIDs, string(entity.DataAppTypeDataapi))
	if err != nil {
		return err
	}
	tilog.Info(ctx, "Begin refresh dataapi app list", zap.Int("count", len(appList)))

	for _, dataApp := range appList {
		appID := dataApp.ID
		if count, err := repository.GetDataAppDeploymentLogCount(ctx, appID); err != nil {
			tilog.Error(ctx, "GetDataAppDeploymentLogCount failed", zap.Uint64("dataapp_id", appID), zap.Error(err))
			continue
		} else if count > 0 {
			tilog.Info(ctx, "dataapp already has deployment history", zap.Uint64("dataapp_id", appID))
			continue
		}
		if _, err := TriggerDataAppUpdateFromUI(ctx, appID, "system", ""); err != nil {
			tilog.Error(ctx, "TriggerDataAppUpdateFromUI failed", zap.Uint64("dataapp_id", appID), zap.Error(err))
		} else {
			tilog.Info(ctx, "TriggerDataAppUpdateFromUI succeed", zap.Uint64("dataapp_id", appID))
		}
	}
	tilog.Info(ctx, "End refresh dataapi app list")
	return nil
}

func ExportDataappToPostmanCollection(ctx context.Context, dataAppExtID string) (*dto.DataappPostmanCollection, error) {

	dataappInfo, err := repository.GetDataAppByExtId(ctx, dataAppExtID)
	if err != nil {
		return nil, err
	}
	dataappID := dataappInfo.ID

	draftEndpoints, err := repository.ListEndpoints(ctx, dataappID)
	if err != nil {
		return nil, err
	}

	deployedEndpoints, err := repository.ListAppPublishedEndpoint(ctx, dataappID)
	if err != nil {
		return nil, err
	}
	if err := fillPublishedEndpoints(ctx, deployedEndpoints); err != nil {
		return nil, err
	}

	dataappClusters, err := repository.ListAppClusterByAppId(ctx, dataappInfo.ID)
	if err != nil {
		return nil, err
	}
	clusterIDs := make([]uint64, 0, len(draftEndpoints))
	for _, dataappCluster := range dataappClusters {
		clusterIDs = append(clusterIDs, dataappCluster.ClusterID)
	}
	clusterMap, err := grpc.ListClusterByIdsWPC(ctx, clusterIDs)
	if err != nil {
		return nil, err
	}

	collection := new(dto.DataappPostmanCollection)
	collection.Info.Description = defaultCollectionDesc
	collection.Info.Name = "TiDB Cloud Data Service - " + dataappInfo.AppName
	collection.Info.Schema = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"

	collection.Auth.Type = "digest"
	collection.Auth.Digest = append(collection.Auth.Digest, dto.DataappPostmanVal{
		Key:   "username",
		Value: "{{PUBLIC_KEY}}",
		Type:  "string",
	}, dto.DataappPostmanVal{
		Key:   "password",
		Value: "{{PRIVATE_KEY}}",
		Type:  "string",
	})

	collection.Variable = append(collection.Variable, dto.DataappPostmanVal{
		Key:  "PUBLIC_KEY",
		Type: "string",
	}, dto.DataappPostmanVal{
		Key:  "PRIVATE_KEY",
		Type: "string",
	})

	regionSuccessfulDomainsMap, err := GetRegionSuccessfulDomainsMap(ctx, dataappID)
	if err != nil {
		return nil, err
	}

	deployedFolder := dto.DataappPostmanCollectionFolder{Name: "Deployed"}
	systemFolder := dto.DataappPostmanCollectionFolder{Name: "System"}
	deployedMap := make(map[uint64]*entity.EndpointPublish)
	for _, val := range deployedEndpoints {
		endpoint := val.ToEndpoint()
		clusterInfo := clusterMap[endpoint.ClusterId]
		if clusterInfo == nil && len(clusterIDs) == 1 {
			clusterInfo = clusterMap[clusterIDs[0]]
		}
		api, err := convertEndpointToPostmanAPI(ctx, dataappInfo, endpoint, clusterInfo, false, regionSuccessfulDomainsMap)
		if err != nil {
			tilog.Error(ctx, "convert deployed endpoint to postman api failed", zap.Uint64("endpoint_id", endpoint.ID), zap.Error(err))
			continue
		}
		if endpoint.IsSystemDataEndpoint() {
			systemFolder.Item = append(systemFolder.Item, api)
		} else {
			deployedFolder.Item = append(deployedFolder.Item, api)
		}
		deployedMap[val.EndpointId] = val
	}
	if len(deployedFolder.Item) > 0 {
		collection.Item = append(collection.Item, deployedFolder)
	}
	if len(systemFolder.Item) > 0 {
		collection.Item = append(collection.Item, systemFolder)
	}

	draftFolder := dto.DataappPostmanCollectionFolder{Name: "Draft"}
	for _, endpoint := range draftEndpoints {
		if !checkEndpointCanTestByPostman(ctx, endpoint) {
			continue
		}
		clusterInfo := clusterMap[endpoint.ClusterId]
		if clusterInfo == nil && len(clusterIDs) == 1 {
			clusterInfo = clusterMap[clusterIDs[0]]
		}
		if deployedEndpoint := deployedMap[endpoint.ID]; deployedEndpoint != nil && endpoint.EqualPublished(deployedEndpoint) {
			continue
		}
		api, err := convertEndpointToPostmanAPI(ctx, dataappInfo, endpoint, clusterInfo, true, regionSuccessfulDomainsMap)
		if err != nil {
			tilog.Error(ctx, "convert draft endpoint to postman api failed", zap.Uint64("endpoint_id", endpoint.ID), zap.Error(err))
			continue
		}
		draftFolder.Item = append(draftFolder.Item, api)
	}
	if len(draftFolder.Item) > 0 {
		collection.Item = append(collection.Item, draftFolder)
	}

	return collection, nil
}

func convertEndpointToPostmanAPI(ctx context.Context, dataappInfo *entity.DataApp, endpoint *entity.Endpoint, cluster *core.Cluster, isDraft bool, regionSuccessfulDomainsMap map[string]*entity.CustomDomain) (*dto.DataappPostmanAPI, error) {
	var regionName string
	if cluster != nil {
		regionName = cluster.RegionName
	}
	api := dto.DataappPostmanAPI{Name: endpoint.Name}
	api.ProtocolProfileBehavior.FollowAuthorizationHeader = true
	api.ProtocolProfileBehavior.FollowRedirects = true
	api.ProtocolProfileBehavior.FollowOriginalHttpMethod = true
	api.Request.Method = endpoint.Method
	pathSegments := strings.Split(endpoint.Endpoint, "/")
	for idx, segment := range pathSegments {
		n := len(segment)
		if n > 2 && segment[0] == '{' && segment[n-1] == '}' {
			pathSegments[idx] = ":" + segment[1:n-1]
		}
	}
	path := strings.Join(pathSegments, "/")
	api.Request.Url = GenAPPServiceUrl(dataappInfo, regionName, regionSuccessfulDomainsMap) + path
	api.Request.Header = append(api.Request.Header, dto.DataappPostmanVal{
		Key:   "Content-Type",
		Value: "application/json",
		Type:  "text",
	})
	if isDraft {
		api.Request.Header = append(api.Request.Header, dto.DataappPostmanVal{
			Key:   "endpoint-type",
			Value: "draft",
			Type:  "text",
		})
	}
	var args []entity.EndpointArgs
	if endpoint.Args != "" {
		if err := utils.FromJson(endpoint.Args, &args); err != nil {
			return nil, errs.NewInternalGeneralBusiness("failed to parse endpoint arguments", err)
		}
	}
	query, body, _ := convertEndpointRequest(endpoint.Type, endpoint.Method, endpoint.Endpoint, args, endpoint.IsBatch(), nil)
	if query != "" {
		api.Request.Url += "?" + query
	}
	if body != "" {
		api.Request.Body = &dto.DataappPostmanBody{
			Mode: "raw",
			Raw:  body,
			Options: map[string]interface{}{
				"raw": map[string]interface{}{
					"language": "json",
				},
			},
		}
	}
	return &api, nil
}

func DataappHasEndpointCanTest(ctx context.Context, dataappID uint64) (bool, error) {

	draftEndpoints, err := repository.ListEndpoints(ctx, dataappID)
	if err != nil {
		return false, err
	}

	for _, endpoint := range draftEndpoints {
		if checkEndpointCanTestByPostman(ctx, endpoint) {
			return true, nil
		}
	}
	return false, nil
}

func checkEndpointCanTestByPostman(ctx context.Context, endpoint *entity.Endpoint) bool {
	if !endpointPathRegexp.MatchString(endpoint.Endpoint) {
		tilog.Info(ctx, "filter out the endpoint for invalid endpoint path")
		return false
	}
	if !validMethod[endpoint.Method] {
		tilog.Info(ctx, "filter out the endpoint for invalid endpoint path")
		return false
	}
	if !endpoint.IsSystemDataEndpoint() {
		if endpoint.ClusterId == 0 {
			tilog.Info(ctx, "filter out the endpoint for empty cluster id")
			return false
		}
		if endpoint.SqlTemplate == "" {
			tilog.Info(ctx, "filter out the endpoint for empty sql content")
			return false
		}
	}
	return true
}

var defaultCollectionDesc = `# TiDB Cloud Data Service
TiDB Cloud is the fully-managed service offering of TiDB, an advanced, open-source, distributed SQL database that provides real-time access to transactional data with minimal latency and extensive horizontal scalability. TiDB Serverless is the Serverless tier of TIDB Cloud that enables developers to deploy their infrastructure at scale in the most cost-efficient way without manual provisioning and the best part is,  It's absolutely free!

If you haven't created a TiDB Serverless cluster yet or want to explore more, you can do it now by following this link: [Create TiDB Serverless Cluster](https://tidbcloud.com/free-trial).

## Getting Started

Assuming you have completed the necessary [steps](https://docs.pingcap.com/tidbcloud/data-service-postman-integration) to import the Data App as a collection into Postman, you're now ready to explore the endpoints and optimize them for your development and testing needs. The endpoints exist in the following two folders:

- The **Deployed** folder : Contains all deployed endpoints within your Data App.

- The **Draft** folder : Contains endpoints that are modified and/or not deployed yet.

- The **System** folder : Contains predefined system-provided endpoints.

Occasionally, you may find an endpoint that appears in both the "Deployed" and "Draft" folders. This is mainly because there are  two versions of the same endpoint, with the one in the "Draft" folder currently under review.

### Setting up API Key:
To leverage this integration fully, you must provide API Key values for two specific Postman variables: PUBLIC_KEY and PRIVATE_KEY. Please follow the instructions provided in the [API Key creation guide](https://docs.pingcap.com/tidbcloud/data-service-api-key#create-an-api-key) to complete the setup.

### Syncing Updates:
Please note that any updates to the data app in TiDB Cloud Data Services will not synchronize automatically with Postman. To keep up with the latest changes, you'll need to reiterate the import process in Postman. After the import, you will be required to setup  the postman variables (PUBLIC_KEY and PRIVATE_KEY) again for a seamless integration experience.

For more information, refer to our [documentation](https://docs.pingcap.com/tidbcloud/data-service-postman-integration)

Happy development!
`

func GetSampleDataappStates(ctx context.Context, dataappID uint64) (string, []entity.SampleDataTaskRecord, error) {
	dataapp, err := repository.GetDataApp(dataappID)
	if err != nil {
		return "", nil, err
	}

	appStatus := dataapp.GetStatus()

	sampleTask, err := repository.GetSampleDataappTask(ctx, dataappID)
	if err != nil {
		if tiErrs.IsError(err, tiErrs.ErrNotFound) {
			return appStatus, nil, nil
		}
		return "", nil, err
	}

	var taskContent entity.SampleDataTaskContent
	if err := sampleTask.UnmarshalContent(&taskContent); err != nil {
		return "", nil, errs.NewInternalGeneralBusiness("invalid sample dataapp task content", err)
	}

	taskRecords := make([]entity.SampleDataTaskRecord, 0, len(taskContent.States))
	stateExists := make(map[string]bool, len(taskContent.States))
	for idx, state := range taskContent.States {
		if stateExists[state.State] {
			continue
		}
		if idx < len(taskContent.States)-1 &&
			(state.State == entity.SampleDataTaskStateFaile || state.State == entity.SampleDataTaskStateSuccee) {
			continue
		}
		if state.State == entity.SampleDataTaskStateFaile {
			if sampleTask.Status != entity.CommonTaskStatusFailed {
				continue
			}
			state.Message = sampleTask.ErrMessage
		}
		taskRecords = append(taskRecords, state)
		stateExists[state.State] = true
	}
	return appStatus, taskRecords, nil
}

func UpdateSampleDataappStatus(ctx context.Context, dataappID uint64) error {
	return repository.UpdateDataAppStatus(ctx, dataappID, entity.DataAppStatusNormal)
}

func BatchGetCodeExample(ctx context.Context, dataappID uint64, operator string) ([]dto.EndpointCodeExample, error) {

	dataapp, err := repository.GetDataApp(dataappID)
	if err != nil {
		return nil, err
	}

	publishedEndpoints, err := repository.ListAppPublishedEndpoint(ctx, dataappID)
	if err != nil {
		return nil, err
	} else if len(publishedEndpoints) == 0 {
		return nil, nil
	}

	var exampleInput entity.CodeExampleInput
	exampleInput.FillDefaultVal = true

	apikeys, err := RbacListApiKeys(ctx, dataappID, false)
	if err != nil {
		tilog.Error(ctx, "RbacListApiKeys failed", zap.Uint64("dataapp_id", dataappID), zap.Error(err))
		return nil, err
	}
	if len(apikeys) == 0 {
		apikey, err := RbacCreateApikey(ctx, dataappID, "SampleAppKey", constants.ApikeyRoleReadonly, entity.DataAPISourceDataAPIConsole, operator, 100, account.OperatorType_OPERATOR_TYPE_USER, 0)
		if err != nil {
			tilog.Error(ctx, "RbacCreateApikey failed", zap.Uint64("dataapp_id", dataappID), zap.Error(err))
			return nil, err
		}
		exampleInput.AK = apikey.AccessKey
		exampleInput.SK = apikey.SecretKey
	} else {
		exampleInput.AK = apikeys[0].AccessKey
		exampleInput.SK = apikeys[0].SecretKey
	}

	regionSuccessfulDomainsMap, err := GetRegionSuccessfulDomainsMap(ctx, dataappID)
	if err != nil {
		return nil, err
	}

	result := make([]dto.EndpointCodeExample, 0, len(publishedEndpoints))
	for _, endpoint := range publishedEndpoints {
		exampleInput.ClusterID = strconv.FormatUint(endpoint.ClusterId, 10)
		codeExample := getEndpointCodeExample(ctx, dataapp, endpoint.ToEndpoint(), false, exampleInput, regionSuccessfulDomainsMap)
		result = append(result, dto.EndpointCodeExample{
			Path:     endpoint.Endpoint,
			Method:   endpoint.Method,
			Examples: codeExample,
		})
	}
	return result, nil
}

func HasProjectRWPermission(ctx context.Context, orgID, projectID, userID uint64) (bool, error) {
	resourceURI := fmt.Sprintf(`orgs/%d/projects/%d/dataservices/read_write_cluster`, orgID, projectID)
	rbacRequest := account.RbacPermissionRequest{UserId: userID, ResourceUri: resourceURI}
	rbacResponse, err := grpc.HasPermissionForUser(ctx, &rbacRequest)
	if err != nil {
		return false, errs.NewInternalGeneralBusiness("Failed to check user project read and write permissions, Please try again later.", nil)
	}
	return rbacResponse.AuthResult, nil
}

func ConvertDataappInfo(entityApp *entity.DataApp) *dto.DataAppInfo {
	appVersion := entityApp.AppVersion
	if appVersion == "" {
		appVersion = "1.0.0"
	}
	return &dto.DataAppInfo{
		ID:               entityApp.ID,
		OrgId:            strconv.FormatUint(entityApp.OrgId, 10),
		ProjectId:        strconv.FormatUint(entityApp.ProjectId, 10),
		ExternalAppID:    entityApp.ExternalAppID,
		AppName:          entityApp.AppName,
		AppType:          entityApp.AppType,
		AppVersion:       appVersion,
		CreatedBy:        entityApp.CreatedBy,
		UpdatedBy:        entityApp.UpdatedBy,
		CreatedAt:        entityApp.CreatedAt,
		UpdatedAt:        entityApp.UpdatedAt,
		BaseUrl:          GenAPPServiceUrl(entityApp, "", nil),
		Description:      entityApp.Description,
		DeploymentRegion: entityApp.DeploymentRegion,
		// 20231115 remove oas authorization feature, change this field to always true
		OasAuthorized: true,
		Status:        entityApp.GetStatus(),
		Settings:      entityApp.Settings,
	}
}

func GetSystemEndpointConfig(ctx context.Context, appID uint64) (*dto.SystemEndpointConfigData, error) {

	endpointTypes := make([]string, 0, len(defaultSystemDataEndpoints))
	for _, systemEndpoint := range defaultSystemDataEndpoints {
		endpointTypes = append(endpointTypes, systemEndpoint.Type)
	}
	endpoints, err := repository.ListEndpoints(ctx, appID, endpointTypes...)
	if err != nil {
		return nil, err
	}
	endpointTypeMap := make(map[string]bool, len(endpoints))
	for _, endpoint := range endpoints {
		endpointTypeMap[endpoint.Type] = true
	}

	result := new(dto.SystemEndpointConfigData)
	for _, systemEndpoint := range defaultSystemDataEndpoints {
		result.DataEndpoints = append(result.DataEndpoints, &dto.EndpointConfig{
			Type:      systemEndpoint.Type,
			Key:       systemEndpoint.Key(),
			IsEnabled: endpointTypeMap[systemEndpoint.Type],
		})
	}
	return result, nil
}

func SaveSystemEndpointConfig(ctx context.Context, projectID, appID uint64, systemEndpointConfigData *dto.SystemEndpointConfigData, operator string, credential dto2.Credential) error {

	addEndpointKeyMap := make(map[string]*dto.EndpointConfig, len(systemEndpointConfigData.DataEndpoints))
	delEndpointKeyMap := make(map[string]*dto.EndpointConfig, len(systemEndpointConfigData.DataEndpoints))
	for _, systemEndpointConf := range systemEndpointConfigData.DataEndpoints {
		_, ok := defaultSystemDataEndpointMap[systemEndpointConf.Key]
		if !ok {
			return errs.NewBadRequest("Unsupported endpoint config key:"+systemEndpointConf.Key, nil)
		}
		if systemEndpointConf.IsEnabled {
			addEndpointKeyMap[systemEndpointConf.Key] = systemEndpointConf
		} else {
			delEndpointKeyMap[systemEndpointConf.Key] = systemEndpointConf
		}
	}

	existsEndpointMap := make(map[string]*entity.Endpoint)
	existsEndpoints, err := repository.ListEndpoints(ctx, appID)
	if err != nil {
		return err
	}
	for _, endpoint := range existsEndpoints {
		existsEndpointMap[endpoint.Key()] = endpoint
	}

	var (
		defaultOnce      sync.Once
		defaultClusterID string
		defaultDatabase  string
	)
	getArgsDefaultVal := func() (string, string) {
		defaultOnce.Do(func() {
			var err error
			defer func() {
				if err != nil {
					tilog.Error(ctx, "failed to get system endpoint argument default value", zap.Error(err))
				}
			}()
			clusterList, err := repository.ListAppClusterByAppId(ctx, appID)
			if err != nil {
				return
			}
			var clusterID uint64
			if len(clusterList) > 0 {
				clusterID = clusterList[0].ClusterID
			}
			if clusterID == 0 {
				return
			}
			defaultClusterID = strconv.FormatUint(clusterID, 10)
			dbs, err := ShowDBs(ctx, entity.ShowDBParam{
				ProjectId: projectID,
				ClusterId: clusterID,
				ShowSysDB: "false",
			}, credential)
			if err != nil {
				return
			}
			if len(dbs) > 0 {
				defaultDatabase = dbs[0]
			}
		})
		return defaultClusterID, defaultDatabase
	}

	err = repository.Transaction(ctx, func(ctx context.Context) error {
		addEndpointOps := DeployOperation{OpType: entity.GenSnapshotTypeEndpointDeploy}
		delEndpointOps := DeployOperation{OpType: entity.GenSnapshotTypeEndpointUndeploy}
		for _, systemEndpointConf := range addEndpointKeyMap {
			if existsEndpointMap[systemEndpointConf.Key] != nil {
				if !existsEndpointMap[systemEndpointConf.Key].IsSystemDataEndpoint() {
					return errs.NewBadRequest(fmt.Sprintf(`Endpoint with path "%s" already exists under the dataapp`, systemEndpointConf.Key), nil)
				}
				continue
			}
			systemEndpoint := defaultSystemDataEndpointMap[systemEndpointConf.Key]
			systemEndpoint.DataappId = appID
			systemEndpoint.CreatedBy = operator
			systemEndpoint.UpdatedBy = operator
			clusterID, database := getArgsDefaultVal()
			systemEndpoint.Args = fmt.Sprintf(systemEndpoint.Args, clusterID, database)
			if _, err := repository.CreateEndpoint(ctx, &systemEndpoint, nil); err != nil {
				return err
			}
			existsEndpointMap[systemEndpointConf.Key] = &systemEndpoint
			addEndpointOps.TargetIDs = append(addEndpointOps.TargetIDs, systemEndpoint.ID)
			tilog.Info(ctx, "add one system endpoint", zap.String("endpoint_key", systemEndpointConf.Key))
		}
		for _, systemEndpointConf := range delEndpointKeyMap {
			systemEndpoint := existsEndpointMap[systemEndpointConf.Key]
			if systemEndpoint == nil {
				continue
			}
			if !systemEndpoint.IsSystemDataEndpoint() {
				return errs.NewBadRequest(fmt.Sprintf(`There is no Execute Query Endpoint with path "%s", unable to deleted`, systemEndpointConf.Key), nil)
			}
			if err := repository.DeleteEndpoint(ctx, systemEndpoint.ID, operator); err != nil {
				return err
			}
			delEndpointOps.TargetIDs = append(delEndpointOps.TargetIDs, systemEndpoint.ID)
			tilog.Info(ctx, "delete one system endpoint", zap.String("endpoint_key", systemEndpointConf.Key))
		}
		var ops []DeployOperation
		if len(addEndpointOps.TargetIDs) > 0 {
			ops = append(ops, addEndpointOps)
			// push sync endpoint event
			if err = publishMultiEndpointEvent(ctx, addEndpointOps.TargetIDs, operator, dto.EndpointActionSave); err != nil {
				return err
			}
		}
		if len(delEndpointOps.TargetIDs) > 0 {
			ops = append(ops, delEndpointOps)
			// push delete endpoint event
			if err = publishMultiEndpointEvent(ctx, delEndpointOps.TargetIDs, operator, dto.EndpointActionDelete); err != nil {
				return err
			}
		}
		tilog.Info(ctx, "SaveSystemEndpointConfig trigger operations", zap.Any("ops", ops), zap.Uint64("dataapp_id", appID), zap.String("user", operator))
		if len(ops) > 0 {
			if _, err := TriggerDeployWithOps(ctx, appID, ops, operator, "Deployment for modify endpoint library"); err != nil {
				return err
			}
		}
		return nil
	})
	return err
}

func UpdateDataAppLogResSwitch(ctx context.Context, appID uint64, on bool) error {
	err := repository.Transaction(ctx, func(ctx context.Context) error {
		dataApp, err := repository.GetDataAppWithLock(ctx, appID)
		if err != nil {
			return err
		}
		if dataApp.Settings.LogExecResult == on {
			return nil
		}
		dataApp.Settings.LogExecResult = on
		if err := repository.UpdateDataAppSettings(ctx, dataApp); err != nil {
			return err
		}
		return publishDataappEvent(ctx, appID, "", dto.DataappActionUpdateLogResSwitch)
	})
	if err != nil {
		tilog.Error(ctx, "UpdateDataAppLogResSwitch failed", zap.Uint64("dataapp_id", appID), zap.Error(err))
	}
	return err
}
