package service

import (
	"context"
	"dataservices/commons/config"
	comerrs "dataservices/commons/errs"
	"dataservices/commons/github"
	"dataservices/commons/metrics"
	"dataservices/commons/utils"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"time"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var (
	githubDirectoryRegexp = regexp.MustCompile(`^/$|^/[\p{L}\p{N}-_]+(/[\p{L}\p{N}-_]+)*$`)
)

func ConvertDataAppSnapShotFromString(ctx context.Context, str string) (*entity.DataAppSnapshot, error) {
	var res entity.DataAppSnapshot
	// tilog.Info(ctx, "snapshot is "+str)
	err := json.Unmarshal([]byte(str), &res)
	if err != nil {
		return nil, err
	}
	return &res, nil
}

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

func GetLatestDataAppDeployLog(ctx context.Context, appId uint64) (*entity.DataAppDeploymentLog, error) {
	logs, err := ListDataAppDeployLogs(ctx, appId, 1)
	if err != nil {
		return nil, err
	}
	if len(logs) != 1 {
		return nil, errs.NewBadRequest(fmt.Sprintf("no latest log found for %d", appId), nil)
	}
	return logs[0], nil
}

func ListDataAppDeployLogs(ctx context.Context, appId uint64, limit int) ([]*entity.DataAppDeploymentLog, error) {
	return repository.ListDataAppDeploymentLogs(ctx, appId, limit)
}

func GetDataAppDeployLogs(ctx context.Context, appId, logId uint64) (*entity.DataAppDeploymentLog, error) {
	result, err := repository.GetDataAppDeploymentLog(ctx, logId)
	if err != nil {
		return nil, err
	} else if result.DataappID != appId {
		return nil, errs.NewBadRequest("The dataapp deployment not found", nil)
	}
	return result, nil
}

func ListDataAppDeployLogsWithPager(ctx context.Context, appId uint64, preID uint64, pageSize int) ([]*entity.DataAppDeploymentLog, error) {
	return repository.ListDataAppDeploymentLogsWithPager(ctx, appId, preID, pageSize)
}

func SaveDataAppDeploymentName(ctx context.Context, deploymentLogID uint64, deploymentName string) error {
	return repository.UpdateDataAppDeployLogName(ctx, deploymentLogID, deploymentName)
}

// NOTE(bing.wang) GenDataAppSnapshotEndpoints
//
//	deployDataApp = listEndpoints
//	deployEndpoint = listPublishEndpoints + current_endpoint
//	undeployEndpoint = listPublishEndpoints - current_endpoint
func GenDataAppSnapShotEndpoints(ctx context.Context, appId uint64, ops []DeployOperation) ([]*entity.Endpoint, error) {
	if len(ops) == 0 {
		return nil, errs.NewInternalServerError("GenDataAppSnapShotEndpoints operation list can not be empty!", nil)
	}

	var endpointMap map[uint64]*entity.Endpoint
	getBaseEndpointMap := func(mustDraft bool) (map[uint64]*entity.Endpoint, error) {
		if mustDraft {
			endpoints, err := repository.ListEndpoints(ctx, appId)
			if err != nil {
				return nil, err
			}

			endpointMap = make(map[uint64]*entity.Endpoint)
			for _, endpoint := range endpoints {
				if err := checkEndpointCanRunWithoutCluster(ctx, endpoint); err != nil {
					continue
				}
				if ok, err := checkEndpointCanDeployedAutomatic(endpoint.ID); err != nil {
					return nil, err
				} else if !ok {
					continue
				}
				endpointMap[endpoint.ID] = endpoint
			}
			return endpointMap, nil
		}

		if endpointMap != nil {
			return endpointMap, nil
		}

		publishs, err := repository.ListAppPublishedEndpoint(ctx, appId)
		if err != nil {
			return nil, err
		}
		if err = fillPublishedEndpoints(ctx, publishs); err != nil {
			return nil, err
		}
		endpointMap = make(map[uint64]*entity.Endpoint)
		for _, val := range publishs {
			endpointMap[val.EndpointId] = val.ToEndpoint()
		}
		return endpointMap, nil
	}

	var err error
	for _, op := range ops {
		switch op.OpType {
		case entity.GenSnapshotTypeDataAppDeploy:
			endpointMap, err = getBaseEndpointMap(true)
			if err != nil {
				return nil, err
			}
		case entity.GenSnapshotTypeEndpointDeploy:
			endpointMap, err = getBaseEndpointMap(false)
			if err != nil {
				return nil, err
			}
			endpointList, err := repository.ListEndpointByIDsExcludeDeleted(ctx, appId, op.TargetIDs)
			if err != nil {
				return nil, err
			}
			for _, endpoint := range endpointList {
				endpointMap[endpoint.ID] = endpoint
			}
		case entity.GenSnapshotTypeEndpointUndeploy, entity.GenSnapshotTypeEndpointDelete:
			endpointMap, err = getBaseEndpointMap(false)
			if err != nil {
				return nil, err
			}
			for _, endpointID := range op.TargetIDs {
				delete(endpointMap, endpointID)
			}
		default:
			endpointMap, err = getBaseEndpointMap(false)
			if err != nil {
				return nil, err
			}
		}
	}
	result := make([]*entity.Endpoint, 0, len(endpointMap))
	for _, endpoint := range endpointMap {
		result = append(result, endpoint)
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i].ID < result[j].ID
	})
	return result, nil
}

func GenDataAppSnapShot(ctx context.Context, snapshotType entity.GenSnapshotType, appId uint64, endpointIds []uint64) (*entity.DataAppSnapshot, error) {
	return GenDataAppSnapShotWithOps(ctx, appId, []DeployOperation{
		{
			OpType:    snapshotType,
			TargetIDs: endpointIds,
		},
	})
}

func GenDataAppSnapShotWithOps(ctx context.Context, appId uint64, ops []DeployOperation) (*entity.DataAppSnapshot, error) {
	var res entity.DataAppSnapshot
	var err error
	res.DataApp, err = repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, err
	}
	res.Endpoints, err = GenDataAppSnapShotEndpoints(ctx, appId, ops)
	if err != nil {
		return nil, err
	}
	res.Clusters, err = repository.ListAppClusterByAppId(ctx, appId)
	if err != nil {
		return nil, err
	}
	return &res, nil
}

func EnableDataAppGithubAutoDeploy(ctx context.Context, appID uint64, input entity.PreflightLinkGithubParam, enableAutoDeploy bool) error {
	if input.InstallationId == 0 || input.RepoId == 0 || input.Branch == "" || input.Directory == "" {
		return errs.NewBadRequest("bad parameters", nil)
	}
	// validate directory
	if !githubDirectoryRegexp.MatchString(input.Directory) {
		return errs.NewBadRequest("The directory must start with a forward slash, followed by a sequence of one or more directory names. Each directory name must only contain letters, numbers, hyphens(-), underscores(_). Trailing slash are not allowed", nil)
	}
	if len(input.Directory) > 255 {
		return errs.NewBadRequest("The directory should contain 255 characters at most", nil)
	}
	input.Directory = filepath.Clean(input.Directory)
	// validate installation id
	repo, err := github.GetRepoById(ctx, config.Global().GhappConf.AppId, input.InstallationId, input.RepoId)
	if err != nil {
		return errs.NewBadRequest("get repo failed", err)
	}
	if *repo.Archived {
		return errs.NewBadRequest("repo has been archived", nil)
	}
	// validate branch
	// branch, err := github.GetBranch(ctx, config.Global().GhappConf.AppId, input.InstallationId, *repo.Owner.Login, *repo.Name, input.Branch)
	// if err != nil {
	// 	return errs.NewBadRequest("get branch failed", err)
	// }
	// if *branch.Protected {
	// 	return errs.NewBadRequest("the branch is protected", nil)
	// }
	// validate preflight_request_id
	if input.PreflightRequestId != "" {
		preflightRequestId, err := strconv.ParseUint(input.PreflightRequestId, 10, 64)
		if err != nil {
			return errs.NewBadRequest("invalid preflight_request_id", err)
		}
		dataapp, err := repository.GetDataAppWithCtx(ctx, appID)
		if err != nil {
			return errs.NewBadRequest("get dataapp failed", err)
		}
		err = ValidatePreflightRequestId(ctx, dataapp.OrgId, dataapp.ProjectId, preflightRequestId)
		if err != nil {
			return err
		}
		auth, err := repository.GetGhappAuthorizationByPreflightRequestId(ctx, preflightRequestId)
		if err != nil {
			return errs.NewBadRequest("validate preflight request auth failed", err)
		}
		if auth.DataappId != 0 {
			return errs.NewBadRequest("preflight request already linked to dataapp", nil)
		}
		err = repository.UpdatePreflightGhappAuthorizationDataappId(ctx, preflightRequestId, appID)
		if err != nil {
			return errs.NewInternalDB("update preflight request auth failed", err)
		}
	}
	// validate repo/branch/directory
	validateRes, err := ValidateRepoBranchDir(ctx, appID, 0, input.RepoId, input.Branch, input.Directory)
	if err != nil {
		return err
	}
	if validateRes.Conflict {
		return errs.NewConflict(fmt.Sprintf("config conflict with `%s`", validateRes.ExternalAppID), nil)
	}

	var enableUIDrafts bool
	setting, err := repository.GetDataAppDeploySettingRaw(ctx, appID)
	if err != nil || enableAutoDeploy {
		enableUIDrafts = true
	} else {
		enableUIDrafts = setting.EnableUIDrafts
	}

	return SaveDataAppDeploymentSetting(ctx, appID, &entity.DataAppDeploymentConfig{
		PreflightLinkGithubParam: input,
		DeploymentSwithConfig: entity.DeploymentSwithConfig{
			EnableUIDrafts:   enableUIDrafts,
			EnableAutoDeploy: enableAutoDeploy,
		},
	})
}

func SaveDataAppDeploymentSetting(ctx context.Context, dataAppID uint64, in *entity.DataAppDeploymentConfig) error {
	setting, err := repository.GetDataAppDeploySetting(ctx, dataAppID)
	if err != nil {
		return err
	}

	setting.EnableAutoDeploy = in.EnableAutoDeploy
	setting.EnableUIDrafts = in.EnableUIDrafts
	setting.GithubInstallationId = in.InstallationId
	setting.GithubRepoID = in.RepoId
	setting.GithubBranch = in.Branch
	setting.GithubDirectory = in.Directory
	if in.InstallationId == 0 {
		setting.Provider = ""
	} else {
		setting.Provider = string(entity.DataAppDeploymentLogOriginGithub)
	}
	// if setting.GithubInstallationId != in.InstallationId ||
	// 	setting.GithubRepoID != in.RepoId ||
	// 	setting.GithubBranch != in.Branch ||
	// 	setting.GithubDirectory != in.Directory {
	// 	setting.EnableAutoDeploy = false
	// }
	_, err = repository.SaveDataAppDeploymentSetting(ctx, setting)
	return err
}

func SetDataAppHasDeployConflict(ctx context.Context, appID uint64, hasDeployConflict bool) error {
	setting, err := repository.GetDataAppDeploySetting(ctx, appID)
	if err != nil {
		return err
	}
	setting.HasDeployConflict = hasDeployConflict
	_, err = repository.SaveDataAppDeploymentSetting(ctx, setting)
	return err
}

func SaveDataAppDeploymentSettingSwithConfig(ctx context.Context, dataappId uint64, config *entity.DeploymentSwithConfig) error {
	setting, err := repository.GetDataAppDeploySetting(ctx, dataappId)
	if err != nil {
		return err
	}
	if config.EnableAutoDeploy && setting.GithubInstallationId == 0 {
		return errs.NewBadRequest("should connect to github before enable automatic deployment", nil)
	}
	setting.EnableUIDrafts = config.EnableUIDrafts
	setting.EnableAutoDeploy = config.EnableAutoDeploy
	_, err = repository.SaveDataAppDeploymentSetting(ctx, setting)
	return err
}

func TriggerEndpointDeployFromUI(ctx context.Context, appID uint64, endpointID uint64, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	if deployName == "" {
		deployName = "Deployment from Endpoint"
	}
	return TriggerDeployFromUI(ctx, appID, endpointID, entity.GenSnapshotTypeEndpointDeploy, createdBy, deployName)
}

func TriggerEndpointUndeployFromUI(ctx context.Context, appID uint64, endpointID uint64, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	if deployName == "" {
		deployName = "Deployment from Endpoint"
	}
	return TriggerDeployFromUI(ctx, appID, endpointID, entity.GenSnapshotTypeEndpointUndeploy, createdBy, deployName)
}

func TriggerClusterCreateFromUI(ctx context.Context, appID uint64, clusterIDs []uint64, createdBy string) (*entity.DataAppDeploymentLog, error) {
	return TriggerDeployFromUI(ctx, appID, 0, entity.GenSnapshotTypeClusterCreate, createdBy, "Deployment from Cluster")
}

func TriggerClusterDeleteFromUI(ctx context.Context, appID uint64, clusterID uint64, createdBy string) (*entity.DataAppDeploymentLog, error) {
	return TriggerDeployFromUI(ctx, appID, 0, entity.GenSnapshotTypeClusterDelete, createdBy, "Deployment from Cluster")
}

func TriggerDataAppUpdateFromUI(ctx context.Context, appID uint64, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	return TriggerDeployFromUI(ctx, appID, 0, entity.GenSnapshotTypeDataAppUpdate, createdBy, deployName)
}

func TriggerDataAppDeployFromUI(ctx context.Context, appID uint64, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	return TriggerDeployFromUI(ctx, appID, 0, entity.GenSnapshotTypeDataAppDeploy, createdBy, deployName)
}
func TriggerEndpointDeleteFromUI(ctx context.Context, appID uint64, endpointId uint64, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	return TriggerDeployFromUI(ctx, appID, endpointId, entity.GenSnapshotTypeEndpointDelete, createdBy, deployName)
}

type DeployOperation struct {
	OpType    entity.GenSnapshotType
	TargetIDs []uint64
}

func TriggerDeployWithOps(ctx context.Context, appID uint64, ops []DeployOperation, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	if err := CheckDataAppCanDeploy(ctx, appID); err != nil {
		return nil, err
	}
	snapshot, err := GenDataAppSnapShotWithOps(ctx, appID, ops)
	if err != nil {
		return nil, err
	}
	return TriggerDeployWithSnapshot(ctx, appID, createdBy, deployName, snapshot)
}

func TriggerDeployFromUI(ctx context.Context, appID, endpointID uint64, snapshotType entity.GenSnapshotType, createdBy string, deployName string) (*entity.DataAppDeploymentLog, error) {
	start := time.Now()
	var err error
	defer func() {
		if !utils.IsTestUser(createdBy) {
			ok := err == nil || comerrs.IsClientErr(err)
			metrics.DeployDuration.WithLabelValues(strconv.FormatBool(ok)).Observe(time.Since(start).Seconds())
		}
	}()

	if snapshotType == entity.GenSnapshotTypeEndpointDeploy || snapshotType == entity.GenSnapshotTypeEndpointUndeploy {
		if err := CheckDataAppCanDeploy(ctx, appID); err != nil {
			return nil, err
		}
	}
	snapShotPtr, err := GenDataAppSnapShot(ctx, snapshotType, appID, []uint64{endpointID})
	if err != nil {
		return nil, err
	}
	deploymentLog, err := TriggerDeployWithSnapshot(ctx, appID, createdBy, deployName, snapShotPtr)
	if err != nil {
		return nil, err
	}
	if snapshotType == entity.GenSnapshotTypeDataAppDeploy {
		SetDataAppHasDeployConflict(ctx, appID, false)
	}
	return deploymentLog, nil
}

func TriggerDeployWithSnapshot(ctx context.Context, appID uint64, createdBy string, deployName string, snapshot *entity.DataAppSnapshot) (*entity.DataAppDeploymentLog, error) {
	if deployName == "" {
		deployName = "Deployment from Data App"
	}

	appInfo, err := GetDataApp(ctx, appID)
	if err != nil {
		return nil, err
	}

	if appInfo.AppType == entity.DataAppTypeChat2Query {
		return nil, nil
	}

	if err := BatchCheckEndpointCanRun(ctx, snapshot.Endpoints); err != nil {
		return nil, errs.NewBadRequest("Some Endpoints cannot run: "+err.Error(), err)
	}
	snapShotJsonString, _ := json.Marshal(snapshot)
	deploymentLog := &entity.DataAppDeploymentLog{
		DataappID:          appID,
		Name:               deployName,
		Origin:             entity.DataAppDeploymentLogOriginUI,
		CommitID:           "",
		DiffUrl:            "",
		CreatedBy:          createdBy,
		Status:             entity.DataAppDeploymentStatusPending,
		StatusErrorMessage: "",
		Snapshot:           string(snapShotJsonString),
		DeployedAt:         utils.Now(),
	}
	deploymentLog, err = WriteDataAppDeploymentLogAndNotifyWorker(ctx, deploymentLog)
	if err != nil {
		return nil, err
	}
	return deploymentLog, nil
}

func TriggerAutoGenerateDeployFromUI(ctx context.Context, appID uint64, endpointIDs []uint64, snapshotType entity.GenSnapshotType, createdBy string) (*entity.DataAppDeploymentLog, error) {
	deployName := "Auto Generate Endpoints Deployment from Data App"
	if err := CheckDataAppCanDeploy(ctx, appID); err != nil {
		return nil, err
	}
	snapShotPtr, err := GenDataAppSnapShot(ctx, snapshotType, appID, endpointIDs)
	if err != nil {
		return nil, err
	}
	if err := BatchCheckEndpointCanRun(ctx, snapShotPtr.Endpoints); err != nil {
		return nil, errs.NewBadRequest("Some Endpoints cannot run: "+err.Error(), err)
	}
	snapShotJsonString, _ := json.Marshal(snapShotPtr)
	deploymentLog := &entity.DataAppDeploymentLog{
		DataappID:          appID,
		Name:               deployName,
		Origin:             entity.DataAppDeploymentLogOriginUI,
		CommitID:           "",
		DiffUrl:            "",
		CreatedBy:          createdBy,
		Status:             entity.DataAppDeploymentStatusPending,
		StatusErrorMessage: "",
		Snapshot:           string(snapShotJsonString),
		DeployedAt:         utils.Now(),
	}
	deploymentLog, err = WriteDataAppDeploymentLogAndNotifyWorker(ctx, deploymentLog)
	if err != nil {
		return nil, err
	}
	SetDataAppHasDeployConflict(ctx, appID, false)

	return deploymentLog, nil
}

func TriggerRedeploy(ctx context.Context, deployLogID uint64, createdBy, deploymentName string) (*entity.DataAppDeploymentLog, error) {
	deployLog, err := repository.GetDataAppDeploymentLog(ctx, deployLogID)
	if err != nil {
		return nil, err
	}
	if err := CheckDataAppHasDeployConflict(ctx, deployLog.DataappID); err != nil {
		return nil, err
	}
	if deploymentName == "" {
		deploymentName = deployLog.Name
	}

	// check endpoint can run
	snapshot, err := ConvertDataAppSnapShotFromString(ctx, deployLog.Snapshot)
	if err != nil {
		return nil, errs.NewBadRequest("parse snapshot failed", err)
	}
	for _, endpoint := range snapshot.Endpoints {
		if err := checkEndpointCanRun(ctx, endpoint); err != nil {
			return nil, errs.NewBadRequest(fmt.Sprintf(`Endpoint "%s" check failed: %s`, endpoint.Name, err.Error()), err)
		}
	}

	// check cluster status
	clusterIDs := make([]uint64, 0, len(snapshot.Clusters))
	for _, cluster := range snapshot.Clusters {
		clusterIDs = append(clusterIDs, cluster.ClusterID)
	}
	if len(clusterIDs) > 0 {
		if err := checkClusterStatus(ctx, snapshot.DataApp.OrgId, snapshot.DataApp.ProjectId, clusterIDs, createdBy); err != nil {
			return nil, err
		}
	}

	deployLog.ID = 0
	deployLog.Name = deploymentName
	deployLog.Status = entity.DataAppDeploymentStatusPending
	deployLog.StatusErrorMessage = ""
	deployLog.CreatedBy = createdBy
	deployLog.CreatedAt = utils.Now()
	deployLog.UpdatedAt = utils.Now()
	deployLog.DeployedAt = utils.Now()
	deployLog.RedeployFromID = deployLogID
	deployLog.Origin = entity.DataAppDeploymentLogOriginUI
	deployLog.CommitID = ""
	deployLog.DiffUrl = ""
	return WriteDataAppDeploymentLogAndNotifyWorker(ctx, deployLog)
}

func WriteDataAppDeploymentLogAndNotifyWorker(ctx context.Context, deploymentLog *entity.DataAppDeploymentLog) (*entity.DataAppDeploymentLog, error) {
	// TODO(bing.wang) add eventbus notification
	return repository.CreateDataAppDeploymentLog(ctx, deploymentLog)
}

func DeployDataApp(ctx context.Context, deployLog *entity.DataAppDeploymentLog) (err error) {

	logger := tilog.WithContextL(ctx).With(
		zap.Uint64("deployment_id", deployLog.ID),
		zap.Uint64("dataapp_id", deployLog.DataappID),
	)
	operator := deployLog.CreatedBy

	startAt := time.Now()
	logger.Info("PublishDataApp begin")
	defer func() {
		logger.Info("PublishDataApp completed", zap.String("cost", time.Since(startAt).String()), zap.Error(err))
	}()

	snapshot, err := ConvertDataAppSnapShotFromString(ctx, deployLog.Snapshot)
	if err != nil {
		return errs.NewBadRequest("parse snapshot failed", err)
	}

	dataAppId := deployLog.DataappID
	endpoints := snapshot.Endpoints
	appClusters := snapshot.Clusters
	version := strconv.FormatUint(deployLog.ID, 10)

	if err := BatchCheckEndpointCanRun(ctx, endpoints); err != nil {
		return errs.NewBadRequest("Some Endpoints cannot run: "+err.Error(), err)
	}

	publishs, err := repository.ListAppPublishedEndpoint(ctx, dataAppId)
	if err != nil {
		return errs.NewBadRequest("ListAppPublishedEndpoint failed: "+err.Error(), err)
	}

	currentEndpoints, err := repository.ListEndpoints(ctx, dataAppId)
	if err != nil {
		return errs.NewBadRequest("ListEndpoints failed: "+err.Error(), err)
	}

	oldAppClusters, err := repository.ListAppClusterByAppId(ctx, dataAppId)
	if err != nil {
		return err
	}

	logger.Info("begin transaction to do deploy")
	return repository.Transaction(ctx, func(ctx context.Context) error {

		// update dataapp
		if err := repository.UpdateDataApp(ctx, snapshot.DataApp); err != nil {
			return err
		}

		var deployEvent dto.DataappDeployEvent
		deployEvent.DataappID = dataAppId

		// endpoint draft alignment
		endpointDraftDiff := CompareEndpointList(ctx, currentEndpoints, endpoints)
		logger.Info("CompareEndpointList result",
			zap.Int("add_draft", len(endpointDraftDiff.Added)),
			// zap.Int("modify_draft", len(endpointDraftDiff.Modified)),
			zap.Int("nochange_draft", len(endpointDraftDiff.NoChange)),
		)
		for _, endpoint := range endpointDraftDiff.Added {
			logger.Info("add endpoints draft", zap.Uint64("endpoint_id", endpoint.ID))
			if result, err := RecoverEndpoint(ctx, endpoint, operator); err != nil {
				return err
			} else {
				deployEvent.EndpointRequest.AddIDs = append(deployEvent.EndpointRequest.AddIDs, result.AddID)
				deployEvent.EndpointRequest.DeleteIDs = append(deployEvent.EndpointRequest.DeleteIDs, result.DeleteIDs...)
			}
		}
		// for _, endpoint := range endpointDraftDiff.Modified {
		// 	logger.Info("modify endpoints draft", zap.Uint64("endpoint_id", endpoint.ID))
		// 	if _, err := repository.SaveEndpoint(ctx, endpoint, nil); err != nil {
		// 		return err
		// 	}
		// 	deployEvent.EndpointRequest.ModifyIDs = append(deployEvent.EndpointRequest.ModifyIDs, endpoint.ID)
		// }

		// endpoint deployment alignment
		endpointDiff := CompareEndpointsWithPublishList(ctx, endpoints, publishs)
		logger.Info("CompareEndpointsWithPublishList result",
			zap.Int("deploy", len(endpointDiff.Added)),
			zap.Int("redeploy", len(endpointDiff.Modified)),
			zap.Int("undeploy", len(endpointDiff.DeletedPublish)),
		)
		for _, endpoint := range endpointDiff.Added {
			logger.Info(fmt.Sprintf("[DataApp-%d] Deploy Added Endpoints. %d", dataAppId, endpoint.ID))
			if err := repository.CreateOnlineEndpoint(ctx, endpoint, version, operator); err != nil {
				return err
			}
			deployEvent.EndpointRequest.DeployIDs = append(deployEvent.EndpointRequest.DeployIDs, endpoint.ID)
		}
		for _, endpoint := range endpointDiff.Modified {
			logger.Info(fmt.Sprintf("[DataApp-%d] Deploy Modified Endpoints. %d", dataAppId, endpoint.ID))
			if err := repository.CreateOnlineEndpoint(ctx, endpoint, version, operator); err != nil {
				return err
			}
			deployEvent.EndpointRequest.DeployIDs = append(deployEvent.EndpointRequest.DeployIDs, endpoint.ID)
		}
		for _, publish := range endpointDiff.DeletedPublish {
			logger.Info(fmt.Sprintf("[DataApp-%d] Deploy Deleted Endpoints. %d", dataAppId, publish.EndpointId))
			if err := repository.OfflineEndpoint(ctx, publish.EndpointId, operator); err != nil {
				return err
			}
			deployEvent.EndpointRequest.UndeployIDs = append(deployEvent.EndpointRequest.UndeployIDs, publish.EndpointId)
		}

		// app cluster alignment
		clusterDiff := CompareAppClusters(ctx, oldAppClusters, appClusters)
		logger.Info("CompareAppClusters result",
			zap.Int("add", len(clusterDiff.Added)),
			zap.Int("delete", len(clusterDiff.Deleted)),
		)
		if len(clusterDiff.Added) > 0 {
			logger.Info("create app cluster", zap.Uint64s("cluster_ids", clusterDiff.Added))
			if err := repository.MultiCreateAppClusterWithIDs(ctx, dataAppId, clusterDiff.Added, operator); err != nil {
				return err
			}
			deployEvent.ClusterRequest.AddIDs = append(deployEvent.ClusterRequest.AddIDs, clusterDiff.Added...)
		}
		if len(clusterDiff.Deleted) > 0 {
			logger.Info("delete app cluster", zap.Uint64s("cluster_ids", clusterDiff.Deleted))
			if err := repository.MultiDeleteAppCluster(ctx, dataAppId, clusterDiff.Deleted, operator); err != nil {
				return err
			}
			deployEvent.ClusterRequest.DeleteIDs = append(deployEvent.ClusterRequest.DeleteIDs, clusterDiff.Deleted...)
		}

		// Publish an eventbus message to notify handler to sync data app all data to Dynamodb
		if err := publishDataappDeployEvent(ctx, deployEvent, operator); err != nil {
			return err
		}

		return nil
	})
}

func DiscardDataAppDraft(ctx context.Context, deployLog *entity.DataAppDeploymentLog, operator string) (err error) {

	logger := tilog.WithContextL(ctx).With(
		zap.Uint64("deployment_id", deployLog.ID),
		zap.Uint64("dataapp_id", deployLog.DataappID),
		zap.String("user", operator),
	)

	startAt := time.Now()
	logger.Info("DiscardDataAppDraft begin")
	defer func() {
		logger.Info("DiscardDataAppDraft completed", zap.String("cost", time.Since(startAt).String()), zap.Error(err))
	}()

	snapshot, err := ConvertDataAppSnapShotFromString(ctx, deployLog.Snapshot)
	if err != nil {
		return errs.NewBadRequest("parse deployment snapshot failed", err)
	}

	dataAppId := deployLog.DataappID
	endpoints := snapshot.Endpoints
	appClusters := snapshot.Clusters

	if err := BatchCheckEndpointCanRun(ctx, endpoints); err != nil {
		return errs.NewBadRequest("Some Endpoints cannot run: "+err.Error(), err)
	}

	endpointDrafts, err := repository.ListEndpoints(ctx, dataAppId)
	if err != nil {
		return errs.NewBadRequest("ListEndpoints failed: "+err.Error(), err)
	}

	oldAppClusters, err := repository.ListAppClusterByAppId(ctx, dataAppId)
	if err != nil {
		return err
	}

	logger.Info("begin transaction to do discard")
	return repository.Transaction(ctx, func(ctx context.Context) error {

		// update dataapp
		if err := repository.UpdateDataApp(ctx, snapshot.DataApp); err != nil {
			return err
		}

		var deployEvent dto.DataappDeployEvent
		deployEvent.DataappID = dataAppId

		// endpoint draft alignment
		endpointDraftDiff := CompareEndpointList(ctx, endpointDrafts, endpoints)
		logger.Info("CompareEndpointList result",
			zap.Int("add_draft", len(endpointDraftDiff.Added)),
			zap.Int("modify_draft", len(endpointDraftDiff.Modified)),
			zap.Int("delete_draft", len(endpointDraftDiff.Deleted)),
			zap.Int("nochange_draft", len(endpointDraftDiff.NoChange)),
		)
		for _, endpoint := range endpointDraftDiff.Added {
			logger.Info("create endpoints draft", zap.Uint64("endpoint_id", endpoint.ID))
			if result, err := RecoverEndpoint(ctx, endpoint, operator); err != nil {
				return err
			} else {
				deployEvent.EndpointRequest.AddIDs = append(deployEvent.EndpointRequest.AddIDs, result.AddID)
				deployEvent.EndpointRequest.DeleteIDs = append(deployEvent.EndpointRequest.DeleteIDs, result.DeleteIDs...)
			}
		}
		for _, endpoint := range endpointDraftDiff.Modified {
			logger.Info("modify endpoints draft", zap.Uint64("endpoint_id", endpoint.ID))
			if _, err := repository.SaveEndpoint(ctx, endpoint, nil); err != nil {
				return err
			}
			deployEvent.EndpointRequest.ModifyIDs = append(deployEvent.EndpointRequest.ModifyIDs, endpoint.ID)
		}
		if len(endpointDraftDiff.Deleted) > 0 {
			deleteEndpointIDs := make([]uint64, 0, len(endpointDraftDiff.Deleted))
			for _, endpoint := range endpointDraftDiff.Deleted {
				deleteEndpointIDs = append(deleteEndpointIDs, endpoint.ID)
			}
			logger.Info("delete endpoints draft", zap.Uint64s("endpoint_ids", deleteEndpointIDs))
			if err := repository.BatchDeleteEndpoint(ctx, deleteEndpointIDs, operator); err != nil {
				return err
			}
			deployEvent.EndpointRequest.DeleteIDs = append(deployEvent.EndpointRequest.DeleteIDs, deleteEndpointIDs...)
		}

		// app cluster alignment
		clusterDiff := CompareAppClusters(ctx, oldAppClusters, appClusters)
		logger.Info("CompareAppClusters result",
			zap.Int("add", len(clusterDiff.Added)),
			zap.Int("delete", len(clusterDiff.Deleted)),
		)
		if len(clusterDiff.Added) > 0 {
			logger.Info("create app cluster", zap.Uint64s("cluster_ids", clusterDiff.Added))
			if err := repository.MultiCreateAppClusterWithIDs(ctx, dataAppId, clusterDiff.Added, operator); err != nil {
				return err
			}
			deployEvent.ClusterRequest.AddIDs = append(deployEvent.ClusterRequest.AddIDs, clusterDiff.Added...)
		}
		if len(clusterDiff.Deleted) > 0 {
			logger.Info("delete app cluster", zap.Uint64s("cluster_ids", clusterDiff.Deleted))
			if err := repository.MultiDeleteAppCluster(ctx, dataAppId, clusterDiff.Deleted, operator); err != nil {
				return err
			}
			deployEvent.ClusterRequest.DeleteIDs = append(deployEvent.ClusterRequest.DeleteIDs, clusterDiff.Deleted...)
		}

		// Publish an eventbus message to notify handler to sync data app all data to Dynamodb
		if err := publishDataappDeployEvent(ctx, deployEvent, operator); err != nil {
			return err
		}

		return nil
	})
}

func DiscardEndpointWithDeploymentLog(ctx context.Context, deployLog *entity.DataAppDeploymentLog, endpointID uint64, operator string) (err error) {

	logger := tilog.WithContextL(ctx).With(
		zap.Uint64("deployment_id", deployLog.ID),
		zap.Uint64("dataapp_id", deployLog.DataappID),
		zap.Uint64("endpoint_id", endpointID),
		zap.String("user", operator),
	)

	startAt := time.Now()
	logger.Info("DiscardEndpointDraft begin")
	defer func() {
		logger.Info("DiscardEndpointDraft completed", zap.String("cost", time.Since(startAt).String()), zap.Error(err))
	}()

	snapshot, err := ConvertDataAppSnapShotFromString(ctx, deployLog.Snapshot)
	if err != nil {
		return errs.NewBadRequest("parse deployment snapshot failed", err)
	}
	endpoints := snapshot.Endpoints

	var endpoint *entity.Endpoint
	for i := range endpoints {
		if endpoints[i].ID == endpointID {
			endpoint = endpoints[i]
		}
	}

	return repository.Transaction(ctx, func(ctx context.Context) error {
		if endpoint != nil {
			if _, err := repository.SaveEndpoint(ctx, endpoint, nil); err != nil {
				return err
			}
			if err := publishEndpointEvent(ctx, endpointID, operator, dto.EndpointActionSave); err != nil {
				return err
			}
		} else {
			if err := repository.DeleteEndpoint(ctx, endpointID, operator); err != nil {
				return err
			}
			if err := publishEndpointEvent(ctx, endpointID, operator, dto.EndpointActionDelete); err != nil {
				return err
			}
		}
		return nil
	})
}

func CompareAppClusters(ctx context.Context, oldAppClusters, newAppClusters []*entity.AppCluster) *entity.AppClusterDiff {

	oldAppClusterMap := make(map[uint64]bool, len(oldAppClusters))
	newAppClusterMap := make(map[uint64]bool, len(newAppClusters))
	for _, appCluster := range oldAppClusters {
		oldAppClusterMap[appCluster.ClusterID] = true
	}
	for _, appCluster := range newAppClusters {
		newAppClusterMap[appCluster.ClusterID] = true
	}

	var clusterDiff entity.AppClusterDiff
	for clusterID := range newAppClusterMap {
		if !oldAppClusterMap[clusterID] {
			clusterDiff.Added = append(clusterDiff.Added, clusterID)
		}
	}
	for clusterID := range oldAppClusterMap {
		if !newAppClusterMap[clusterID] {
			clusterDiff.Deleted = append(clusterDiff.Deleted, clusterID)
		}
	}
	return &clusterDiff
}

func CompareDataApp(ctx context.Context, before *entity.DataApp, after *entity.DataApp) *entity.DataAppDiff {
	var dataAppDiff entity.DataAppDiff
	if !before.Equal(after) {
		dataAppDiff.Modify = after
	}
	return &dataAppDiff
}

func CompareEndpoints(ctx context.Context, before []*entity.Endpoint, after []*entity.Endpoint) *entity.EndpointsDiff {
	var beforePublish []*entity.EndpointPublish
	for _, val := range before {
		beforePublish = append(beforePublish, val.ToPublished())
	}
	return CompareEndpointsWithPublishList(ctx, after, beforePublish)
}

func CompareEndpointsWithPublishList(ctx context.Context, endpoints []*entity.Endpoint, publishs []*entity.EndpointPublish) *entity.EndpointsDiff {
	var res entity.EndpointsDiff

	var endpointMap = make(map[uint64]*entity.Endpoint)
	var publishMap = make(map[uint64]*entity.EndpointPublish)
	for _, endpoint := range endpoints {
		endpointMap[endpoint.ID] = endpoint
	}
	for _, publish := range publishs {
		publishMap[publish.EndpointId] = publish
	}

	for k, endpoint := range endpointMap {
		if publish, exists := publishMap[k]; !exists {
			res.Added = append(res.Added, endpoint)
		} else {
			if endpoint.EqualPublished(publish) {
				res.NoChange = append(res.NoChange, endpoint)
			} else {
				res.Modified = append(res.Modified, endpoint)
				res.ModifiedPublish = append(res.ModifiedPublish, publish)
				if diff, err := endpoint.DifffDetail(publish); err != nil {
					tilog.Error(ctx, "get endpoint diff detail failed", zap.Uint64("endpoint_id", endpoint.ID), zap.Error(err))
				} else {
					res.ModifiedDetail = append(res.ModifiedDetail, diff)
				}
			}
		}
	}
	// XXX(bing.wang) check deleted endpoint
	for k, publish := range publishMap {
		if _, exists := endpointMap[k]; !exists {
			res.DeletedPublish = append(res.DeletedPublish, publish)
		}
	}
	return &res
}

func CompareEndpointList(ctx context.Context, oldList []*entity.Endpoint, newList []*entity.Endpoint) *entity.EndpointsDiff {
	var res entity.EndpointsDiff

	var oldMap = make(map[uint64]*entity.Endpoint, len(oldList))
	var newMap = make(map[uint64]*entity.Endpoint, len(newList))
	for _, endpoint := range oldList {
		oldMap[endpoint.ID] = endpoint
	}
	for _, endpoint := range newList {
		newMap[endpoint.ID] = endpoint
	}

	for id, newEndpoint := range newMap {
		if oldEndpoint, exists := oldMap[id]; !exists {
			res.Added = append(res.Added, newEndpoint)
		} else if oldEndpoint.Equal(newEndpoint) {
			res.NoChange = append(res.NoChange, newEndpoint)
		} else {
			res.Modified = append(res.Modified, newEndpoint)
		}
	}
	for id, oldEndpoint := range oldMap {
		if _, exists := newMap[id]; !exists {
			res.Deleted = append(res.Deleted, oldEndpoint)
		}
	}
	return &res
}

func GetLatestSuccDeployLog(ctx context.Context, appId uint64) (*entity.DataAppDeploymentLog, error) {
	deploymentLog, err := repository.GetLatestSuccDataAppDeploymentLog(ctx, appId, "")
	if err == nil {
		return deploymentLog, nil
	}
	if !errs.IsError(err, errs.ErrNotFound) {
		return nil, err
	}

	snapShotPtr, err := GenDataAppSnapShot(ctx, entity.GenSnapshotTypeDataAppUpdate, appId, []uint64{0})
	if err != nil {
		return nil, err
	}
	snapShotJson, err := json.Marshal(snapShotPtr)
	if err != nil {
		return nil, errs.NewInternalGeneralBusiness("failed to generate lasest snapshot", err)
	}

	return &entity.DataAppDeploymentLog{
		DataappID:  appId,
		Origin:     entity.DataAppDeploymentLogOriginUI,
		Status:     entity.DataAppDeploymentStatusSuccess,
		Snapshot:   string(snapShotJson),
		CreatedAt:  utils.Now(),
		UpdatedAt:  utils.Now(),
		DeployedAt: utils.Now(),
		CreatedBy:  snapShotPtr.DataApp.CreatedBy,
	}, nil
}

func GetPreviousSuccDeployLog(ctx context.Context, appId uint64, before string) (*entity.DataAppDeploymentLog, error) {
	deploymentLog, err := repository.GetLatestSuccDataAppDeploymentLog(ctx, appId, before)
	if err == nil {
		return deploymentLog, nil
	}
	if !errs.IsError(err, errs.ErrNotFound) {
		return nil, err
	}

	return &entity.DataAppDeploymentLog{
		DataappID:  appId,
		Origin:     entity.DataAppDeploymentLogOriginUI,
		Status:     entity.DataAppDeploymentStatusSuccess,
		Snapshot:   "{}",
		CreatedAt:  utils.Now(),
		UpdatedAt:  utils.Now(),
		DeployedAt: utils.Now(),
	}, nil
}

func GetLatestSuccDeploySnapshot(ctx context.Context, appId uint64) (*entity.DataAppSnapshot, *entity.DataAppDeploymentLog, error) {
	latestSuccDeployLog, err := GetLatestSuccDeployLog(ctx, appId)
	if err != nil {
		return nil, nil, errs.NewBadRequest("GetLatestDataAppDeployLog failed.", err)
	}
	latestSnapshot, err := ConvertDataAppSnapShotFromString(ctx, latestSuccDeployLog.Snapshot)
	if err != nil {
		return nil, nil, errs.NewBadRequest("ConvertDataAppSnapShotFromString failed.", err)
	}
	if latestSnapshot.DataApp.ID != appId {
		return nil, nil, errs.NewBadRequest("Check snapshot dataapp id failed.", nil)
	}
	return latestSnapshot, latestSuccDeployLog, nil
}

func GenSnapshotDiffObj(ctx context.Context, before *entity.DataAppSnapshot, after *entity.DataAppSnapshot) (*entity.SnapshotDiff, error) {
	res := &entity.SnapshotDiff{
		DataAppDiff:     CompareDataApp(ctx, before.DataApp, after.DataApp),
		EndpointsDiff:   CompareEndpoints(ctx, before.Endpoints, after.Endpoints),
		AppClustersDiff: CompareAppClusters(ctx, before.Clusters, after.Clusters),
		TextDiff:        "",
	}
	res.HasDiff = res.DataAppDiff.HasDiff() || res.EndpointsDiff.HasDiff() || res.AppClustersDiff.HasDiff()
	return res, nil
}

func GenSnapShotDiffText(ctx context.Context, before *entity.DataAppSnapshot, after *entity.DataAppSnapshot) (*entity.SnapshotDiff, error) {
	beforeFileTree, err := github.Snapshot2FileTreeNode(before)
	if err != nil {
		return nil, err
	}
	afterFileTree, err := github.Snapshot2FileTreeNode(after)
	if err != nil {
		return nil, err
	}
	diff := beforeFileTree.Diff(afterFileTree)
	diffString, err := diff.ToString()
	if err != nil {
		return nil, err
	}
	return &entity.SnapshotDiff{TextDiff: diffString, HasDiff: (diffString != "")}, nil
}

func DiscardDataApp(ctx context.Context, dataAppId uint64, operator string) error {
	deploymentLog, err := GetLatestSuccDeployLog(ctx, dataAppId)
	if err != nil {
		return err
	}

	err = DiscardDataAppDraft(ctx, deploymentLog, operator)
	if err != nil {
		return err
	}

	SetDataAppHasDeployConflict(ctx, dataAppId, false)
	return nil
}

func DiscardEndpoint(ctx context.Context, dataAppId, endpointID uint64, operator string) error {
	deploymentLog, err := GetLatestSuccDeployLog(ctx, dataAppId)
	if err != nil {
		return err
	}

	return DiscardEndpointWithDeploymentLog(ctx, deploymentLog, endpointID, operator)
}

func CheckDataAppEnableAutoDeploy(ctx context.Context, dataAppID uint64) bool {
	setting, err := repository.GetDataAppDeploySetting(ctx, dataAppID)
	if err != nil {
		tilog.Error(ctx, "failed to get deployment setting", zap.Uint64("dataAppId", dataAppID), zap.Error(err))
		return false
	}
	return setting.EnableAutoDeploy
}

func CheckDataAppEnableDraft(ctx context.Context, dataAppID uint64) (bool, error) {
	setting, err := repository.GetDataAppDeploySetting(ctx, dataAppID)
	return setting.EnableUIDrafts, err
}

func CheckDataAppCanDeploy(ctx context.Context, dataAppID uint64) error {
	appInfo, err := GetDataApp(ctx, dataAppID)
	if err != nil {
		return err
	}

	if appInfo.AppType == entity.DataAppTypeChat2Query {
		return nil
	}

	// check dataapp has unfinished deployment
	deployments, err := repository.ListDataAppUnfinishedDeployment(ctx, dataAppID)
	if err != nil {
		return err
	}
	if len(deployments) > 0 {
		return errs.NewBadRequest("the dataapp has unfinished deployments, please try again latter.", nil)
	}
	return nil
}

func CheckDataAppHasDeployConflict(ctx context.Context, dataAppID uint64) error {
	// check dataapp has conflict flag
	setting, err := repository.GetDataAppDeploySetting(ctx, dataAppID)
	if err != nil {
		tilog.Error(ctx, "failed to get deployment setting", zap.Uint64("dataAppId", dataAppID), zap.Error(err))
		return err
	}
	if setting.HasDeployConflict {
		return comerrs.ErrDeploymentConflict
	}
	return nil
}

func GetDiffBetweenDraftAndLastestSuccDeploy(ctx context.Context, dataAppID uint64, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	return GetDiffBetweenDraftEndpointAndLastestSuccDeploy(ctx, dataAppID, 0, entity.GenSnapshotTypeDataAppDeploy, diffType)
}
func GetDiffBetweenEndpointAndLastestSuccDeploy(ctx context.Context, dataAppID, endpointID uint64, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	if endpointID == 0 {
		return GetDiffBetweenDraftEndpointAndLastestSuccDeploy(ctx, dataAppID, 0, entity.GenSnapshotTypeDataAppDeploy, diffType)
	}
	return GetDiffBetweenDraftEndpointAndLastestSuccDeploy(ctx, dataAppID, endpointID, entity.GenSnapshotTypeEndpointDeploy, diffType)
}
func GetDiffBetweenCurrDeployAndLastestSuccDeploy(ctx context.Context, deploymentLogID uint64, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	currDeploymentLog, err := repository.GetDataAppDeploymentLog(ctx, deploymentLogID)
	if err != nil {
		return nil, errs.NewBadRequest("get current deployment log failed.", err)
	}
	currDeploymentSnapshot, err := ConvertDataAppSnapShotFromString(ctx, currDeploymentLog.Snapshot)
	if err != nil {
		return nil, errs.NewBadRequest("convert current deployment snapshot failed.", err)
	}
	currDeploymentSnapshot.Normalize()

	latestSuccDeploySnapshot, _, err := GetLatestSuccDeploySnapshot(ctx, currDeploymentLog.DataappID)
	if err != nil {
		return nil, errs.NewBadRequest("GetLatestDataAppDeploySnapshot failed.", err)
	}
	latestSuccDeploySnapshot.Normalize()

	return GetDiffBetweenSnapshot(ctx, latestSuccDeploySnapshot, currDeploymentSnapshot, diffType)
}
func GetDiffBetweenCurrDeployAndPreviousSuccDeploy(ctx context.Context, deploymentLogID uint64, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	currDeploymentLog, err := repository.GetDataAppDeploymentLog(ctx, deploymentLogID)
	if err != nil {
		return nil, errs.NewBadRequest("get current deployment log failed.", err)
	}
	currDeploymentSnapshot, err := ConvertDataAppSnapShotFromString(ctx, currDeploymentLog.Snapshot)
	if err != nil {
		return nil, errs.NewBadRequest("convert current deployment snapshot failed.", err)
	}
	currDeploymentSnapshot.Normalize()

	previousDeploymentLog, err := GetPreviousSuccDeployLog(ctx, currDeploymentLog.DataappID, currDeploymentLog.CreatedAt.FormatToStr())
	if err != nil {
		return nil, errs.NewBadRequest("get previous deployment log failed.", err)
	}
	previousDeploymentSnapshot, err := ConvertDataAppSnapShotFromString(ctx, previousDeploymentLog.Snapshot)
	if err != nil {
		return nil, errs.NewBadRequest("get previous deployment snapshot failed.", err)
	}
	previousDeploymentSnapshot.Normalize()

	return GetDiffBetweenSnapshot(ctx, previousDeploymentSnapshot, currDeploymentSnapshot, diffType)
}
func GetDiffBetweenDraftEndpointAndLastestSuccDeploy(ctx context.Context, dataAppID, endpointID uint64, snapType entity.GenSnapshotType, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	curSnapShot, err := GenDataAppSnapShot(ctx, snapType, dataAppID, []uint64{endpointID})
	if err != nil {
		return nil, errs.NewBadRequest("GetDataAppSnapShot failed.", err)
	}
	curSnapShot.Normalize()

	latestSuccDeploySnapshot, deploymentLog, err := GetLatestSuccDeploySnapshot(ctx, dataAppID)
	if err != nil {
		return nil, errs.NewBadRequest("GetLatestDataAppDeploySnapshot failed.", err)
	}
	latestSuccDeploySnapshot.Normalize()

	diff, err := GetDiffBetweenSnapshot(ctx, latestSuccDeploySnapshot, curSnapShot, diffType)
	if err != nil {
		return nil, err
	}
	diff.DeploymentLog = deploymentLog
	return diff, nil
}
func GetDiffBetweenDraftEndpointAndSubmitData(ctx context.Context, dataAppID uint64, endpointData *entity.CreateEndpointParam, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	endpointDraft, err := repository.GetEndpointWithCtx(ctx, endpointData.EndpointID)
	if err != nil {
		return nil, err
	}

	settings, _ := json.Marshal(endpointData.Settings)
	var args []byte
	if endpointData.Args != nil {
		args, _ = json.Marshal(endpointData.Args)
	}
	rwMode := getSqlRwMode(ctx, endpointData.SqlTemplate)
	endpointSubmit := &entity.Endpoint{
		ID:          endpointData.EndpointID,
		DataappId:   dataAppID,
		Name:        endpointData.Name,
		Description: endpointData.Description,
		Endpoint:    endpointData.Endpoint,
		Method:      endpointData.Method,
		ClusterId:   endpointData.ClusterId,
		Database:    endpointData.Database,
		SqlTemplate: endpointData.SqlTemplate,
		Settings:    string(settings),
		Args:        string(args),
		Type:        entity.EndpointTypeChat2Sql,
		RWMode:      rwMode,
		Tag:         endpointData.Tag,
		Batch:       endpointData.Batch,
	}

	oldSnap := entity.DataAppSnapshot{Endpoints: []*entity.Endpoint{endpointSubmit}}
	newSnap := entity.DataAppSnapshot{Endpoints: []*entity.Endpoint{endpointDraft}}

	diff, err := GetDiffBetweenSnapshot(ctx, &oldSnap, &newSnap, diffType)
	if err != nil {
		return nil, err
	}
	return diff, nil
}

func GetDiffBetweenSnapshot(ctx context.Context, from, to *entity.DataAppSnapshot, diffType entity.SnapshotDiffType) (*entity.SnapshotDiff, error) {
	if diffType != entity.SnapshotDiffTypeText && diffType != entity.SnapshotDiffTypeStruct {
		return nil, errs.NewBadRequest("diffType unsupported "+string(diffType), nil)
	}
	var (
		diff *entity.SnapshotDiff
		err  error
	)
	if diffType == entity.SnapshotDiffTypeStruct {
		diff, err = GenSnapshotDiffObj(ctx, from, to)
	} else if diffType == entity.SnapshotDiffTypeText {
		diff, err = GenSnapShotDiffText(ctx, from, to)
	}
	if err != nil {
		tilog.Error(ctx, "CompareSnapshotDiff failed", zap.Error(err))
		return nil, errs.NewBadRequest("failed to calculate snapshot diff.", err)
	}
	return diff, nil
}

func GetDataAppDeploymentStatus(ctx context.Context, dataAppID uint64) (*entity.DeploymentStatus, error) {
	result := new(entity.DeploymentStatus)
	result.ButtonStatus = entity.ButtonHidden

	deploymentList, err := repository.ListDataAppUnfinishedDeployment(ctx, dataAppID)
	if err != nil {
		return nil, err
	} else if len(deploymentList) > 0 {
		result.Message = "Deploying changes"
		result.ActionType = entity.ActionDoing
		return result, nil
	}

	finishedDeployment, err := repository.GetLastDataAppFinishedDeploymentInTime(ctx, dataAppID, time.Now().Add(-10*time.Second))
	if err != nil {
		if !errs.IsError(err, errs.ErrNotFound) {
			return nil, err
		}
	} else if finishedDeployment.Status == entity.DataAppDeploymentStatusSuccess {
		result.Message = "Last deployment from the " + string(finishedDeployment.Origin) + " succeeded at ${action_time}"
		result.ActionTime = finishedDeployment.FinishedAt
		result.ActionType = entity.ActionSucceed
		result.Origin = finishedDeployment.Origin
		return result, nil
	} else if finishedDeployment.Status == entity.DataAppDeploymentStatusFailed {
		result.Message = "Last deployment from " + string(finishedDeployment.Origin) + " failed at ${action_time}"
		result.ActionTime = finishedDeployment.FinishedAt
		result.ActionType = entity.ActionFailed
		result.Origin = finishedDeployment.Origin
		return result, nil
	}

	if err := CheckDataAppHasDeployConflict(ctx, dataAppID); err != nil {
		if !errs.IsError(err, comerrs.ErrDeploymentConflict) {
			return nil, err
		}
		result.Message = "Changes have been made to the app outside of this draft. Please discard or deploy override."
		result.ButtonStatus = entity.ButtonReviewDraft
		result.ActionType = entity.ActionDoing
		return result, nil
	}

	if diff, err := GetDiffBetweenDraftAndLastestSuccDeploy(ctx, dataAppID, entity.SnapshotDiffTypeStruct); err != nil {
		return nil, err
	} else if diff.HasDiff {
		result.Message = "Changes have been made to your app since the last deploy."
		result.ButtonStatus = entity.ButtonReviewAndDeploy
		if enabled, err := CheckDataAppEnableDraft(ctx, dataAppID); err != nil {
			tilog.Warn(ctx, "CheckDataAppEnableDraft failed", zap.Error(err))
		} else if !enabled {
			result.ButtonStatus = entity.ButtonDeploy
		}
		result.ActionType = entity.ActionDoing
		return result, nil
	}

	return result, nil
}

func checkEndpointCanDeployedAutomatic(endpointID uint64) (bool, error) {
	lastPublish, err := repository.GetLastPublishEvent(endpointID)
	if err != nil {
		if errs.IsError(err, errs.ErrNotFound) {
			return true, nil
		}
		return false, err
	}
	if lastPublish.IsOnline == 0 {
		return false, nil
	}
	return true, nil
}

type RecoverResult struct {
	AddID     uint64
	DeleteIDs []uint64
}

func RecoverEndpoint(ctx context.Context, endpoint *entity.Endpoint, operator string) (*RecoverResult, error) {
	endpointList, err := repository.ListDraftEndpointByNameAndMethod(endpoint.DataappId, endpoint.Method, endpoint.Endpoint)
	if err != nil {
		return nil, err
	}
	var deleteIDs []uint64
	for _, dbEndpoint := range endpointList {
		if endpoint.ID != dbEndpoint.ID {
			deleteIDs = append(deleteIDs, dbEndpoint.ID)
		}
	}
	if len(deleteIDs) > 0 {
		if err := repository.BatchDeleteEndpoint(ctx, deleteIDs, operator); err != nil {
			return nil, err
		}
	}
	if endpoint.Name == "" && endpoint.ID > 0 {
		dbEndpoint, err := repository.GetEndpointWithoutCheck(ctx, endpoint.ID)
		if err != nil {
			return nil, err
		}
		endpoint.Name = dbEndpoint.Name
		endpoint.Description = dbEndpoint.Description
	}
	if _, err := repository.SaveEndpoint(ctx, endpoint, nil); err != nil {
		return nil, err
	}
	return &RecoverResult{
		AddID:     endpoint.ID,
		DeleteIDs: deleteIDs,
	}, nil
}

func fillPublishedEndpoints(ctx context.Context, publishedEndpoints []*entity.EndpointPublish) error {
	if len(publishedEndpoints) == 0 {
		return nil
	}

	endpointIDs := make([]uint64, 0, len(publishedEndpoints))
	for _, publishedEndpoint := range publishedEndpoints {
		endpointIDs = append(endpointIDs, publishedEndpoint.EndpointId)
	}
	endpoints, err := repository.ListEndpointByIDs(ctx, endpointIDs)
	if err != nil {
		return err
	}
	endpointMap := make(map[uint64]*entity.Endpoint, len(publishedEndpoints))
	for _, endpoint := range endpoints {
		endpointMap[endpoint.ID] = endpoint
	}
	for _, publishedEndpoint := range publishedEndpoints {
		endpoint, ok := endpointMap[publishedEndpoint.EndpointId]
		if !ok {
			continue
		}
		publishedEndpoint.Name = endpoint.Name
		publishedEndpoint.Description = endpoint.Description
	}
	return nil
}

func CheckEndpointDiffCanDeploy(ctx context.Context, endpointDiff *entity.EndpointsDiff) error {
	if endpointDiff == nil {
		return nil
	}
	if err := BatchCheckEndpointCanRun(ctx, endpointDiff.Added); err != nil {
		return err
	}
	if err := BatchCheckEndpointCanRun(ctx, endpointDiff.Modified); err != nil {
		return err
	}
	return nil
}

func CheckDataAppHasCluster(ctx context.Context, dataAppID, clusterID uint64) error {
	_, err := repository.GetAppCluster(ctx, dataAppID, clusterID)
	if err == nil {
		return nil
	}
	if errs.IsError(err, errs.ErrNotFound) {
		return errs.NewBadRequest("Cluster is not associated with any DataApp. Please bind the cluster to a DataApp first", nil)
	}
	return err
}
