package taskengine

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/aliyun/aliyun_assist_client/thirdparty/sirupsen/logrus"

	"github.com/aliyun/aliyun_assist_client/agent/commandermanager"
	"github.com/aliyun/aliyun_assist_client/agent/flagging"
	"github.com/aliyun/aliyun_assist_client/agent/log"
	"github.com/aliyun/aliyun_assist_client/agent/pluginmanager"
	"github.com/aliyun/aliyun_assist_client/agent/pluginmanager/acspluginmanager"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/commander"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/host"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/models"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/outputbuffer"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/parameters"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/scriptmanager"
	"github.com/aliyun/aliyun_assist_client/agent/taskengine/taskerrors"
	"github.com/aliyun/aliyun_assist_client/agent/util"
	"github.com/aliyun/aliyun_assist_client/agent/util/paramstore"
	"github.com/aliyun/aliyun_assist_client/agent/util/process"
	"github.com/aliyun/aliyun_assist_client/agent/util/timetool"
	"github.com/aliyun/aliyun_assist_client/common/langutil"
)

const (
	defaultQuoto    = 12000
	defaultQuotoPre = 6000
)

const (
	LAUNCHER_CMDLINE = "LAUNCHER_CMDLINE"
)

var (
	pluginRe = regexp.MustCompile(`@(\S+)`)
)

type FinishCallback func()
type ReportErrorCallback func(taskId string, repeat models.RunTaskRepeatType, errorCode, status string) (isTaskErr bool)

type Task struct {
	taskInfo         models.RunTaskInfo
	scheduleLocation *time.Location
	onFinish         FinishCallback
	onReportError    ReportErrorCallback

	processer               models.TaskProcessor
	startTime               time.Time
	endTime                 time.Time
	monotonicStartTimestamp int64
	monotonicEndTimestamp   int64
	exit_code               int
	canceled                bool
	droped                  int
	cancelMut               sync.Mutex

	disableOutputRingbuffer bool
	outputBuf               outputbuffer.OutputBuf
	data_sended             uint32
}

func NewTask(taskInfo models.RunTaskInfo, scheduleLocation *time.Location, onFinish FinishCallback, onReportError ReportErrorCallback) (*Task, error) {
	timeout, err := strconv.Atoi(taskInfo.TimeOut)
	if err != nil {
		timeout = 3600
	}

	var processor models.TaskProcessor
	var isHostProcessor bool
	if taskInfo.ContainerId != "" || taskInfo.ContainerName != "" {
		annotation := map[string]string{
			"containerId":   taskInfo.ContainerId,
			"containerName": taskInfo.ContainerName,
		}
		processor, err = commander.NewCommanderProcessor(taskInfo, timeout, annotation, commandermanager.ContainerCommanderName)
		if err != nil {
			return nil, err
		}
	} else {
		// Check if launcher it's a commander
		if isCommander, commanderName := checkLauncherCommander(taskInfo.Launcher, taskInfo.TaskId); isCommander {
			// Put all param into annotation
			annotation := map[string]string{
				LAUNCHER_CMDLINE: taskInfo.Launcher,
			}
			processor, err = commander.NewCommanderProcessor(taskInfo, timeout, annotation, commanderName)
			if err != nil {
				return nil, err
			}
		} else {
			processor = &host.HostProcessor{
				TaskId:              taskInfo.TaskId,
				InvokeVersion:       taskInfo.InvokeVersion,
				CommandType:         taskInfo.CommandType,
				Repeat:              taskInfo.Repeat,
				Timeout:             timeout,
				CommandName:         taskInfo.CommandName,
				WorkingDirectory:    taskInfo.WorkingDir,
				Username:            taskInfo.Username,
				WindowsUserPassword: taskInfo.Password,
				TerminationMode:     taskInfo.TerminationMode,
				Launcher:            taskInfo.Launcher,
			}
			isHostProcessor = true
		}

	}

	task := &Task{
		taskInfo:                taskInfo,
		scheduleLocation:        scheduleLocation,
		onFinish:                onFinish,
		onReportError:           onReportError,
		processer:               processor,
		canceled:                false,
		droped:                  0,
		disableOutputRingbuffer: flagging.IsTaskOutputRingbufferDisabled(),
	}
	if task.disableOutputRingbuffer {
		task.outputBuf = &outputbuffer.LegacyOutputBuffer{}
	} else {
		task.outputBuf = &outputbuffer.OutputBuffer{}
	}
	if isHostProcessor && langutil.NeedTransformEncoding() {
		task.outputBuf.SetTransformer(func(s []byte) (d []byte) {
			d, _ = langutil.GbkToUtf8(s)
			return
		})
	}

	return task, nil
}

func checkLauncherCommander(launcher string, taskId string) (isCommander bool, commanderName string) {
	if launcher == "" {
		return false, ""
	}
	if len(launcher) > 1 && launcher[0] == '@' && launcher[1] == '@' {
		// local launcher start with @
		return false, ""
	}
	if launcher[0] != '@' {
		return false, ""
	}
	pluginName := matchPlugin(launcher)
	logger := log.GetLogger().WithFields(logrus.Fields{
		"TaskId": taskId,
	})
	_, err := acspluginmanager.QueryPluginFromLocal(pluginName, pluginmanager.PLUGIN_COMMANDER)
	if err != nil {
		// Not found local
		logger.WithError(err).Errorln("Failed to query commander local")
		_, err := acspluginmanager.QueryPluginFromOnline(pluginName, pluginmanager.PLUGIN_COMMANDER, "")
		// Online query nil
		if err != nil {
			logger.WithError(err).Errorln("Failed to query commander local and online")
			return false, ""
		}
		// There is commander online
		return true, pluginName
	}
	return true, pluginName
}

func matchPlugin(launcher string) string {
	match := pluginRe.FindStringSubmatch(launcher)
	if len(match) > 1 {
		result := match[1]
		return result
	} else {
		return ""
	}
}

func (task *Task) PreCheck(reportVerified bool) error {
	// Reuse specified logger across whole task pre-checking phase
	taskLogger := log.GetLogger().WithFields(logrus.Fields{
		"TaskId":        task.taskInfo.TaskId,
		"InvokeVersion": task.taskInfo.InvokeVersion,
		"Phase":         "Pre-checking",
	})

	if task.taskInfo.CommandType != "RunBatScript" &&
		task.taskInfo.CommandType != "RunPowerShellScript" &&
		task.taskInfo.CommandType != "RunShellScript" {
		task.SendInvalidTask("TypeInvalid", fmt.Sprintf("TypeInvalid_%s", task.taskInfo.CommandType))
		err := fmt.Errorf("Invalid command type: %s", task.taskInfo.CommandType)
		taskLogger.Errorln("TypeInvalid", err.Error())
		return err
	}

	if _, err := base64.StdEncoding.DecodeString(task.taskInfo.Content); err != nil {
		task.SendInvalidTask("CommandContentInvalid", err.Error())
		wrapErr := fmt.Errorf("Invalid command content: decode error: %w", err)
		taskLogger.Errorln("CommandContentInvalid", wrapErr.Error())
		return wrapErr
	}

	if invalidParameter, err := task.processer.PreCheck(); err != nil {
		if validationErr, ok := err.(*taskerrors.CommanderError); ok {
			task.SendInvalidTask(validationErr.SubCode, validationErr.Error())
		} else if validationErr, ok := err.(taskerrors.NormalizedValidationError); ok {
			task.SendInvalidTask(validationErr.Param(), validationErr.Value())
		} else if settingErr, ok := err.(taskerrors.InvalidSettingError); ok {
			task.SendInvalidTask(invalidParameter, fmt.Sprintf("%s: %v", settingErr.ShortMessage(), settingErr.Unwrap()))
		} else {
			task.SendInvalidTask(invalidParameter, err.Error())
		}
		taskLogger.WithError(err).Errorf("Invalid parameter \"%s\" for invocation", invalidParameter)
		return err
	}

	if reportVerified == true {
		if err := task.sendTaskVerified(); err != nil {
			return err
		}
	}
	return nil
}

func (task *Task) Run() (taskerrors.ErrorCode, error) {
	if err := task.PreCheck(false); err != nil {
		if taskError, ok := err.(taskerrors.ExecutionError); ok {
			return taskError.ErrCode(), taskError
		}
		return 0, err
	}

	// Reuse specified logger across whole task running phase
	taskLogger := log.GetLogger().WithFields(logrus.Fields{
		"TaskId":            task.taskInfo.TaskId,
		"InvokeVersion":     task.taskInfo.InvokeVersion,
		"Phase":             "Running",
		"disableRingbuffer": task.disableOutputRingbuffer,
	})
	taskLogger.Info("Run task")

	taskLogger.Info("Prepare script file of task")
	decodeBytes, err := base64.StdEncoding.DecodeString(task.taskInfo.Content)
	if err != nil {
		task.SendError("", taskerrors.WrapErrBase64DecodeFailed, fmt.Sprintf("Base64DecodeFailed: %s", err.Error()))
		return taskerrors.WrapErrBase64DecodeFailed, errors.New("decode error")
	}
	doNotLogScript := false
	content := string(decodeBytes)
	if task.taskInfo.EnableParameter {
		content, err = parameters.ResolveBuiltinParameters(content, task.taskInfo.BuiltinParameters)
		if err != nil {
			if invalidErr, ok := err.(taskerrors.InvalidSettingError); ok {
				task.SendInvalidTask("InvalidEnvironmentParameter", invalidErr.ShortMessage())
			} else if taskErr, ok := err.(taskerrors.ExecutionError); ok {
				task.SendError("", taskErr.ErrCode(), taskErr.Error())
				return taskErr.ErrCode(), err
			} else {
				task.SendError("", taskerrors.WrapErrResolveEnvironmentParameterFailed, err.Error())
			}

			return taskerrors.WrapErrResolveEnvironmentParameterFailed, err
		}

		if strings.Contains(content, "oos-secret") {
			// Do not log script which contains secret params
			doNotLogScript = true
		}
		content, err = paramstore.ReplaceAllParameterStore(content)
		if err != nil {
			task.SendInvalidTask(err.Error(), content)
			return 0, errors.New("ReplaceAllParameterStore error")
		}
	}
	if !doNotLogScript {
		taskLogger.Info("script content:", content)
	}

	switch task.taskInfo.CommandType {
	case "RunBatScript":
		content = "@echo off\r\n" + content

		fallthrough
	case "RunPowerShellScript":
		if !flagging.IsNormalizingCRLFDisabled() {
			content = scriptmanager.NormalizeCRLF(content)
		}
	}

	if err := task.processer.Prepare(content); err != nil {
		taskLogger.WithError(err).Errorln("Failed to prepare command process")
		if commErr, ok := err.(*taskerrors.CommanderError); ok {
			task.SendError("", taskerrors.Stringer(commErr.SubCode), commErr.Error())
			return taskerrors.WrapGeneralError, err
		} else if validationErr, ok := err.(taskerrors.NormalizedValidationError); ok {
			task.SendInvalidTask(validationErr.Param(), validationErr.Value())
			return taskerrors.WrapGeneralError, err
		} else if taskErr, ok := err.(taskerrors.ExecutionError); ok {
			task.SendError("", taskErr.ErrCode(), taskErr.Error())
			return taskErr.ErrCode(), err
		} else if invalidErr, ok := err.(taskerrors.InvalidSettingError); ok {
			task.SendInvalidTask("InvalidEnvironmentParameter", invalidErr.ShortMessage())
			return taskerrors.WrapErrResolveEnvironmentParameterFailed, err
		} else {
			return taskerrors.WrapGeneralError, err
		}

	}

	taskLogger.Info("Prepare command process")
	var stdouterrWriter io.Writer
	totalQuoto := task.taskInfo.Output.LogQuota
	if totalQuoto < defaultQuoto {
		totalQuoto = defaultQuoto
	}
	stdouterrWriter, err = task.outputBuf.Init(defaultQuotoPre, totalQuoto-defaultQuotoPre)
	if err != nil {
		taskLogger.Error("init output buf failed: ", err)
		taskError := taskerrors.NewInitOutputBufError(err)
		task.SendError("", taskError.ErrCode(), taskError.Error())
		return taskError.ErrCode(), err
	}

	task.startTime = time.Now()
	task.monotonicStartTimestamp = timetool.ToAccurateTime(task.startTime.Local())
	if taskError := task.sendTaskStart(); taskError != nil {
		taskLogger.WithError(taskError).Error("Send starting event failed")
		return taskError.ErrCode(), taskError
	}
	taskLogger.Infof("Sent starting event")

	// Replace variable representing states with context and channel operation,
	// to replace dangerous state tranfering operation with straightforward
	// message passing action.
	ctx, stopSendRunning := context.WithCancel(context.Background())
	stoppedSendRunning := make(chan struct{}, 1)
	go func(ctx context.Context, stoppedSendRunning chan<- struct{}) {
		defer close(stoppedSendRunning)
		task.data_sended = 0
		// Running output is not needed to be reported during invocation of
		// periodic tasks. But stoppedSendRunning channel is still needed to be
		// closed correctly.
		if task.taskInfo.Cronat != "" {
			return
		}

		intervalMs := task.taskInfo.Output.Interval
		if intervalMs < 1000 {
			intervalMs = 1000
		}
		ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
		lastReportOutputTime := time.Now()
		defer ticker.Stop()
		for {
			// serve the stop signal from context channel with higher priority
			select {
			case <-ctx.Done():
				return
			default:
				// fallthrough to the next select
			}

			select {
			case <-ticker.C:
				outputPre := task.outputBuf.ReadPre()
				if reported := task.sendRunningOutput(string(outputPre), lastReportOutputTime); reported {
					lastReportOutputTime = time.Now()
				}
				if len(outputPre) > 0 {
					atomic.AddUint32(&task.data_sended, uint32(len(outputPre)))
					taskLogger.Infof("Running output sent: %d bytes", atomic.LoadUint32(&task.data_sended))
				} else {
					taskLogger.Infof("Running output sent: %d bytes, just report running no output sent", atomic.LoadUint32(&task.data_sended))
				}
			case <-ctx.Done():
				return
			}
		}
	}(ctx, stoppedSendRunning)

	taskLogger.Info("Start command process")
	var status int
	task.exit_code, status, err = task.processer.SyncRun(stdouterrWriter, stdouterrWriter, nil)
	if status == process.Success {
		taskLogger.WithFields(logrus.Fields{
			"exitcode":   task.exit_code,
			"extraError": err,
		}).Info("Finished command process")
	} else if status == process.Timeout {
		taskLogger.WithFields(logrus.Fields{
			"attchedError": err,
		}).Info("Terminated command process due to timeout")
	} else if status == process.Fail {
		taskLogger.WithError(err).Info("Failed command process")
	} else {
		taskLogger.WithFields(logrus.Fields{
			"exitcode":     task.exit_code,
			"status":       status,
			"attchedError": err,
		}).Warn("Ended command process with unexpected status")
	}

	// That is, send stopping message to the goroutine sending running output
	stopSendRunning()
	// Wait for the goroutine sending running output to exit
	<-stoppedSendRunning
	task.endTime = time.Now()
	task.monotonicEndTimestamp = timetool.ToAccurateTime(timetool.ToStableElapsedTime(task.endTime, task.startTime).Local())

	postOutput := string(task.outputBuf.ReadAll())
	task.droped = task.outputBuf.Dropped()

	if status == process.Fail {
		if err == nil {
			task.sendOutput("failed", postOutput)
		} else if executionErr, ok := err.(taskerrors.NormalizedExecutionError); ok {
			task.SendError(postOutput, taskerrors.Stringer(executionErr.Code()), executionErr.Description())
		} else if taskErr, ok := err.(taskerrors.ExecutionError); ok {
			task.SendError(postOutput, taskErr.ErrCode(), taskErr.Error())
		} else {
			task.SendError(postOutput, taskerrors.WrapErrExecuteScriptFailed, fmt.Sprintf("ExecuteScriptFailed: %s", err.Error()))
		}
	} else if status == process.Timeout {
		task.sendOutput("timeout", postOutput)
	} else {
		if task.IsCancled() == false {
			task.sendOutput("finished", postOutput)
		}
	}
	endTaskLogger := log.GetLogger().WithFields(logrus.Fields{
		"TaskId":        task.taskInfo.TaskId,
		"InvokeVersion": task.taskInfo.InvokeVersion,
		"Phase":         "Ending",
	})
	endTaskLogger.Info("Sent final output and state")

	if err := task.outputBuf.Uninit(); err != nil {
		endTaskLogger.Error("Task outputbuffer err: ", err)
	} else {
		endTaskLogger.Info("Clean task output")
	}

	if err := task.processer.Cleanup(!flagging.GetTaskKeepScriptFile()); err != nil {
		endTaskLogger.WithError(err).Errorln("Failed to cleanup after command finished")
	}

	// Perform instructed poweroff/reboot action after task finished
	if err := task.processer.SideEffect(); err != nil {
		endTaskLogger.WithError(err).Errorln("Failed to apply side-effect of command after finished")
	}

	return 0, nil
}

func (task *Task) sendTaskVerified() error {
	queryParams := fmt.Sprintf("?taskId=%s&invokeVersion=%d",
		task.taskInfo.TaskId, task.taskInfo.InvokeVersion)
	url := util.GetVerifiedTaskService() + queryParams
	url += task.processer.ExtraLubanParams()
	content, _ := util.HttpPost(url, "", "text")
	if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
		log.GetLogger().WithFields(logrus.Fields{
			"taskId":            task.taskInfo.TaskId,
			"invocationVersion": task.taskInfo.InvokeVersion,
		}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
		if task.onReportError != nil && task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status) {
			err := fmt.Errorf("Server response error[%s] and status[%s] when send task verified", resp.ErrorCode, resp.Status)
			taskError := taskerrors.NewServerResponseError(err)
			return taskError
		}
	}
	return nil
}

func (task *Task) sendTaskStart() taskerrors.ExecutionError {
	if task.taskInfo.Output.SendStart == false {
		return nil
	}
	url := util.GetRunningOutputService()
	url += fmt.Sprintf("?taskId=%s&invokeVersion=%d&start=%s",
		task.taskInfo.TaskId, task.taskInfo.InvokeVersion, strconv.FormatInt(task.monotonicStartTimestamp, 10))
	url += task.wallClockQueryParams()
	url += task.processer.ExtraLubanParams()

	content, _ := util.HttpPost(url, "", "text")
	if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
		log.GetLogger().WithFields(logrus.Fields{
			"taskId":            task.taskInfo.TaskId,
			"invocationVersion": task.taskInfo.InvokeVersion,
		}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
		if task.onReportError != nil && task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status) {
			err := fmt.Errorf("Server response error[%s] when send task start", resp.ErrorCode)
			taskError := taskerrors.NewServerResponseError(err)
			return taskError
		}
	}
	return nil
}

func (task *Task) SendInvalidTask(param string, value string) {
	var extraLubanParams string
	if task.processer != nil {
		extraLubanParams = task.processer.ExtraLubanParams()
	}
	content, _ := reportInvalidTask(task.taskInfo.TaskId, task.taskInfo.InvokeVersion, param, value, extraLubanParams)
	if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
		log.GetLogger().WithFields(logrus.Fields{
			"taskId":            task.taskInfo.TaskId,
			"invocationVersion": task.taskInfo.InvokeVersion,
		}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
		if task.onReportError != nil {
			task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status)
		}
	}
}

func (task *Task) sendOutput(status string, output string) {
	var url string
	if status == "finished" {
		url = util.GetFinishOutputService()
	} else if status == "timeout" {
		url = util.GetTimeoutOutputService()
	} else if status == "failed" {
		url = util.GetErrorOutputService()
	} else {
		return
	}

	url += fmt.Sprintf("?taskId=%s&invokeVersion=%d&start=%s&end=%s&exitCode=%s&dropped=%s",
		task.taskInfo.TaskId, task.taskInfo.InvokeVersion, strconv.FormatInt(task.monotonicStartTimestamp, 10),
		strconv.FormatInt(task.monotonicEndTimestamp, 10), strconv.Itoa(task.exit_code), strconv.Itoa(task.droped))
	url += task.wallClockQueryParams()
	url += task.processer.ExtraLubanParams()

	var err error
	var content string
	content, err = util.HttpPost(url, output, "text")

	for i := 0; i < 3 && err != nil; i++ {
		time.Sleep(time.Duration(2) * time.Second)
		content, err = util.HttpPost(url, output, "text")
	}

	if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
		log.GetLogger().WithFields(logrus.Fields{
			"taskId":            task.taskInfo.TaskId,
			"invocationVersion": task.taskInfo.InvokeVersion,
		}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
		if task.onReportError != nil {
			task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status)
		}
	}

	if task.onFinish != nil {
		task.onFinish()
	}
}

func (task *Task) SendError(output string, errCode fmt.Stringer, errDesc string) {
	safelyTruncatedErrDesc := langutil.SafeTruncateStringInBytes(errDesc, 255)
	escapedErrDesc := url.QueryEscape(safelyTruncatedErrDesc)
	queryString := fmt.Sprintf("?taskId=%s&invokeVersion=%d&start=%d&end=%d&exitCode=%d&dropped=%d&errCode=%s&errDesc=%s",
		task.taskInfo.TaskId, task.taskInfo.InvokeVersion, task.monotonicStartTimestamp,
		task.monotonicEndTimestamp, task.exit_code, task.droped, errCode.String(), escapedErrDesc)
	queryString += task.wallClockQueryParams()
	queryString += task.processer.ExtraLubanParams()

	requestURL := util.GetErrorOutputService() + queryString

	content, err := util.HttpPost(requestURL, output, "text")
	for i := 0; i < 3 && err != nil; i++ {
		time.Sleep(time.Duration(2) * time.Second)
		content, err = util.HttpPost(requestURL, output, "text")
	}
	if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
		log.GetLogger().WithFields(logrus.Fields{
			"taskId":            task.taskInfo.TaskId,
			"invocationVersion": task.taskInfo.InvokeVersion,
		}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
		if task.onReportError != nil {
			task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status)
		}
	}
}

// Cancel the task invocation. If quietly is false, notify server the task is canceled.
func (task *Task) Cancel(quietly bool, taskRunning bool) error {
	task.cancelMut.Lock()
	defer task.cancelMut.Unlock()

	stopResult := stopReasonKilled
	var cancelErr error = nil

	if !task.canceled {
		task.canceled = true
		if taskRunning {
			// Consistent with C++ version, end time of canceled task is set to the time
			// of cancel operation
			task.endTime = time.Now()
			if task.startTime.IsZero() {
				task.monotonicEndTimestamp = timetool.ToAccurateTime(task.endTime.Local())
			} else {
				task.monotonicEndTimestamp = timetool.ToAccurateTime(timetool.ToStableElapsedTime(task.endTime, task.startTime).Local())
			}
		}
		cancelErr = task.processer.Cancel()
		taskLogger := log.GetLogger().WithFields(logrus.Fields{
			"taskId":        task.taskInfo.TaskId,
			"invokeVersion": task.taskInfo.InvokeVersion,
		})
		if cancelErr == nil {
			taskLogger.Info("Task canceled")
		} else {
			taskLogger.WithError(cancelErr).Error("Task canceled failed")
		}
		if cancelErr != nil {
			stopResult = stopFailed
		}
	}

	if !quietly {
		output := string(task.outputBuf.ReadAll())
		task.droped = task.outputBuf.Dropped()
		sendStoppedOutput(task.taskInfo.TaskId, task.taskInfo.InvokeVersion,
			task.monotonicStartTimestamp, task.monotonicEndTimestamp, task.exit_code,
			task.droped, output, stopResult, cancelErr)
	}
	return cancelErr
}

func (task *Task) sendRunningOutput(data string, lastReportTime time.Time) bool {
	if len(data) == 0 && task.taskInfo.Output.SkipEmpty && time.Since(lastReportTime) < time.Minute {
		return false
	}
	url := util.GetRunningOutputService()
	url += fmt.Sprintf("?taskId=%s&invokeVersion=%d&start=%s",
		task.taskInfo.TaskId, task.taskInfo.InvokeVersion, strconv.FormatInt(task.monotonicStartTimestamp, 10))
	url += task.wallClockQueryParams()
	url += task.processer.ExtraLubanParams()

	if content, err := util.HttpPost(url, data, "text"); err == nil {
		if resp := parseTaskReportResp(content); resp != nil && resp.ErrorCode != "" {
			log.GetLogger().WithFields(logrus.Fields{
				"taskId":            task.taskInfo.TaskId,
				"invocationVersion": task.taskInfo.InvokeVersion,
			}).Errorf("Receive errorCode[%s] and status[%s] in response", resp.ErrorCode, resp.Status)
			if task.onReportError != nil && task.onReportError(task.taskInfo.TaskId, task.taskInfo.Repeat, resp.ErrorCode, resp.Status) {
				// The task has started running, we need cancel it if the
				// backend server tells us this task has an error.
				task.Cancel(true, true)
			}
		}
	}
	return true
}

func (task *Task) IsCancled() bool {
	task.cancelMut.Lock()
	defer task.cancelMut.Unlock()
	return task.canceled
}

// ResetCancel reset the canceled flag
func (task *Task) ResetCancel() {
	task.cancelMut.Lock()
	defer task.cancelMut.Unlock()
	task.canceled = false
}

// Generate additional querystring parameters: Unix timestamp of wall clock for
// cron/rate tasks, and timezone name of schedule clock for only cron tasks
func (task *Task) wallClockQueryParams() string {
	switch task.taskInfo.Repeat {
	case models.RunTaskRate:
		return fmt.Sprintf("&currentTime=%d", timetool.GetAccurateTime())
	case models.RunTaskCron:
		if task.scheduleLocation != nil {
			// NOTE: The time stdlib of golang hopelessly mixes nil pointer and
			// pointer to pre-defined utcLoc for some Location methods, e.g.,
			// String(). That is, even `*time.Location(nil).String()` would
			// return "UTC" instead of just panic. Be careful with this!!!
			escapedTimezoneName := url.QueryEscape(task.scheduleLocation.String())
			locatedNow := time.Now().In(task.scheduleLocation)
			_, currentOffsetFromUTC := locatedNow.Zone()
			return fmt.Sprintf("&currentTime=%d&offset=%d&timeZone=%s", timetool.ToAccurateTime(locatedNow), currentOffsetFromUTC, escapedTimezoneName)
		} else {
			currentTime, currentOffsetFromUTC, timezoneName := timetool.NowWithTimezoneName()
			escapedTimezoneName := url.QueryEscape(timezoneName)
			return fmt.Sprintf("&currentTime=%d&offset=%d&timeZone=%s", timetool.ToAccurateTime(currentTime), currentOffsetFromUTC, escapedTimezoneName)
		}
	}

	return ""
}
