package worker

import (
	"context"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	service "dataservices/internal/service/central"
	"strconv"
	"sync"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

type deploymentWorker struct {
	ctx            context.Context
	jobStatusMutex sync.Mutex
	jobStatusMap   map[uint64]bool
}

func NewDeploymentWorker(ctx context.Context) Worker {
	var worker = deploymentWorker{
		ctx:          ctx,
		jobStatusMap: make(map[uint64]bool),
	}
	return &worker
}

func (worker *deploymentWorker) RunningMsInterval() time.Duration {
	return 100 * time.Millisecond
}
func (worker *deploymentWorker) Name() string {
	return "deployment_worker"
}

func (worker *deploymentWorker) Handle(tracer trace.Tracer) {
	pendingDeployLogList, err := repository.ListWaitingDeploymentLogs(worker.ctx)
	if err != nil {
		tilog.Error(worker.ctx, "ListWaitingDeploymentLogs failed.", zap.Error(err))
		return
	}
	needDeployLogList := worker.SelectNeedRunningDeploymentLogs(pendingDeployLogList)
	for _, log := range needDeployLogList {
		go func(ctx context.Context, log *entity.DataAppDeploymentLog) {
			newctx, span := tracer.Start(ctx, "deployment_task_"+strconv.FormatUint(log.ID, 10))
			defer span.End()
			worker.DoDataAppDeploy(newctx, log)
		}(worker.ctx, log)
	}
}

func (worker *deploymentWorker) SelectNeedRunningDeploymentLogs(logList []*entity.DataAppDeploymentLog) []*entity.DataAppDeploymentLog {
	var res []*entity.DataAppDeploymentLog
	uniqMap := make(map[uint64]*entity.DataAppDeploymentLog)

	for _, log := range logList {
		if val, exists := uniqMap[log.DataappID]; !exists {
			uniqMap[log.DataappID] = log
		} else if log.CreatedAt.Before(val.CreatedAt) {
			uniqMap[log.DataappID] = log
		}
	}
	for _, log := range uniqMap {
		res = append(res, log)
	}
	return res
}

func SyncDeploymentToGithub(ctx context.Context, deployLog *entity.DataAppDeploymentLog) error {
	snapshot, err := service.ConvertDataAppSnapShotFromString(ctx, deployLog.Snapshot)
	if err != nil {
		tilog.Error(ctx, "load snapshot failed", zap.Uint64("deploymentLogId", deployLog.ID), zap.Error(err))
		return err
	}
	err = service.SyncSnapshotToGithub(ctx, deployLog.DataappID, deployLog.Name, snapshot)
	if err != nil {
		tilog.Error(ctx, "failed to commit to github", zap.Uint64("deploymentLogId", deployLog.ID), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "sync to github succeed", zap.Uint64("deploymentLogId", deployLog.ID))
	return nil
}

func SyncDeploymentToUIDraft(ctx context.Context, deployLog *entity.DataAppDeploymentLog) error {
	return service.DiscardDataAppDraft(ctx, deployLog, "GitHub")
}

func DeployDataAppToProd(ctx context.Context, deployLog *entity.DataAppDeploymentLog) error {
	return service.DeployDataApp(ctx, deployLog)
}

func (worker *deploymentWorker) DoDataAppDeploy(ctx context.Context, deployLog *entity.DataAppDeploymentLog) {
	// NOTE(bing.wang) if FlagDeployJobRunning failed. it means another deploy job exists, return current deploy job.
	startAt := time.Now()
	tilog.Info(ctx, "DoDataAppDeploy begin", zap.Uint64("dataapp_id", deployLog.DataappID), zap.Uint64("deployment_log_id", deployLog.ID))
	defer func() {
		if err := recover(); err != nil {
			tilog.Error(ctx, "DoDataAppDeploy panic", zap.Uint64("dataapp_id", deployLog.DataappID), zap.String("cost", time.Since(startAt).String()), zap.Any("panic_err", err))
		} else {
			tilog.Info(ctx, "DoDataAppDeploy completed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.String("cost", time.Since(startAt).String()))
		}
	}()

	// Check whether the running job exists in current dataapp_id
	if !worker.FlagDeployJobRunning(deployLog.DataappID) {
		tilog.Info(ctx, "DoDataAppDeploy already running, skip once", zap.Uint64("dataapp_id", deployLog.DataappID))
		return
	}
	defer worker.FlagDeployJobDone(deployLog.DataappID)

	if err := repository.UpdateDataAppDeploymentLogStatus(ctx, deployLog.ID, entity.DataAppDeploymentStatusRunning); err != nil {
		tilog.Error(ctx, "UpdateDataAppDeploymentLogStatus failed", zap.String("to_status", "running"), zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
		return
	}

	var hasConflict bool
	if deployLog.Origin == entity.DataAppDeploymentLogOriginGithub &&
		service.CheckDataAppEnableAutoDeploy(ctx, deployLog.DataappID) {
		snapshotDiff, err := service.GetDiffBetweenDraftAndLastestSuccDeploy(ctx, deployLog.DataappID, entity.SnapshotDiffTypeStruct)
		if err != nil {
			tilog.Error(ctx, "GetDiffBetweenDraftAndLastestSuccDeploy failed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
			return
		}
		isDiffByGithub := service.IsSnapshotObjDiffByGithub(snapshotDiff)
		textDiff, err := service.GetDiffBetweenDraftAndLastestSuccDeploy(ctx, deployLog.DataappID, entity.SnapshotDiffTypeText)
		if err != nil {
			tilog.Error(ctx, "GetDiffBetweenDraftAndLastestSuccDeploy text diff failed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
			return
		}
		if textDiff.HasDiff && !isDiffByGithub {
			tilog.Info(ctx, "github push encounter conflict", zap.String("diff", textDiff.TextDiff), zap.Uint64("dataapp_id", deployLog.DataappID))
			service.SetDataAppHasDeployConflict(ctx, deployLog.DataappID, true)
			hasConflict = true
		}
		tilog.Info(ctx, "Check for conflicts in the draft",
			zap.Uint64("dataapp_id", deployLog.DataappID),
			zap.Uint64("deployment_id", snapshotDiff.DeploymentLog.ID),
			zap.Bool("has_diff", snapshotDiff.HasDiff),
			zap.Bool("is_diff_by_github", isDiffByGithub),
			zap.Bool("has_conflict", hasConflict),
		)
	}

	deployErr := DeployDataAppToProd(ctx, deployLog)
	deployEndAt := time.Now()
	if deployErr != nil {
		deployLog.Status = entity.DataAppDeploymentStatusFailed
		deployLog.StatusErrorMessage = "Deploy Failed: " + deployErr.Error()
		tilog.Error(ctx, "DeployDataAppToProd failed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.String("cost", deployEndAt.Sub(startAt).String()), zap.Error(deployErr))
		if err := repository.UpdateDataAppDeployLog(ctx, deployLog); err != nil {
			tilog.Error(ctx, "UpdateDataAppDeployLog failed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
		}
		if hasConflict {
			if err := service.SetDataAppHasDeployConflict(ctx, deployLog.DataappID, false); err != nil {
				tilog.Error(ctx, "SetDataAppHasDeployConflict to false failed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
			}
		}
		return
	}
	tilog.Info(ctx, "DeployDataAppToProd succeed", zap.Uint64("dataapp_id", deployLog.DataappID), zap.String("cost", deployEndAt.Sub(startAt).String()))
	if service.CheckDataAppEnableAutoDeploy(ctx, deployLog.DataappID) {
		if deployLog.Origin == entity.DataAppDeploymentLogOriginUI {
			deployErr = SyncDeploymentToGithub(ctx, deployLog)
		} else if deployLog.Origin == entity.DataAppDeploymentLogOriginGithub && !hasConflict {
			deployErr = SyncDeploymentToUIDraft(ctx, deployLog)
		}
		if deployErr != nil {
			deployLog.Status = entity.DataAppDeploymentStatusFailed
			deployLog.StatusErrorMessage = "Sync Failed: " + deployErr.Error()
			repository.UpdateDataAppDeployLog(ctx, deployLog)
			return
		}
		syncEndAt := time.Now()
		tilog.Info(ctx, "SyncDeployment completed",
			zap.Uint64("dataapp_id", deployLog.DataappID),
			zap.String("origin", string(deployLog.Origin)),
			zap.String("cost", syncEndAt.Sub(deployEndAt).String()),
			zap.Error(deployErr))
	}
	if err := repository.UpdateDataAppDeploymentLogStatus(ctx, deployLog.ID, entity.DataAppDeploymentStatusSuccess); err != nil {
		tilog.Error(ctx, "UpdateDataAppDeploymentLogStatus failed", zap.String("to_status", "succeed"), zap.Uint64("dataapp_id", deployLog.DataappID), zap.Error(err))
		return
	}
}

func (worker *deploymentWorker) FlagDeployJobRunning(dataAppID uint64) bool {
	worker.jobStatusMutex.Lock()
	defer worker.jobStatusMutex.Unlock()
	if _, exists := worker.jobStatusMap[dataAppID]; exists {
		return false
	}
	worker.jobStatusMap[dataAppID] = true
	return true
}

func (worker *deploymentWorker) FlagDeployJobDone(dataAppID uint64) {
	worker.jobStatusMutex.Lock()
	defer worker.jobStatusMutex.Unlock()
	delete(worker.jobStatusMap, dataAppID)
}
func (worker *deploymentWorker) IsEnable() bool {
	return true
}
