/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"jobarranger2/src/jobarg_agent/managers/agent_manager/agentutils"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/agent"
	"jobarranger2/src/libs/golibs/config_reader/conf"
	"jobarranger2/src/libs/golibs/event"
	"jobarranger2/src/libs/golibs/forker"
	jatcp "jobarranger2/src/libs/golibs/ja_tcp"
	"jobarranger2/src/libs/golibs/logger/logger"
	"jobarranger2/src/libs/golibs/utils"
	wm "jobarranger2/src/libs/golibs/worker_manager"
)

type args struct {
	Daemon     bool
	ConfigPath string
	SocketFile string
}

var (
	arg args
)

const monitorJobsId = "monitorJobs"
const housekeeperId = "housekeeper"

func StartDaemonWorkers(data common.Data) {
	// initValue()
	retriggerEventFile()
	wm.StartWorker(func() { monitorJobs() }, monitorJobsId, 180, 0, string(common.AgentManagerProcess))
	wm.StartWorker(func() { housekeeper() }, housekeeperId, agent.Options.JaBkupLoopTimeout, 0, string(common.AgentManagerProcess))
}

func createJobResultFile(jobRunFilePath, jobFilePath string, isAbort bool) error {
	funcName := "createJobResultFile"
	moveFileBase := filepath.Base(strings.TrimSuffix(jobRunFilePath, ".json"))
	filePattern := moveFileBase + "*"
	jobFileBase := filepath.Base(strings.TrimSuffix(jobFilePath, ".job"))
	errorSubFolderPath := filepath.Join(ErrorFolderPath, jobFileBase)

	if err := createJobResultFileCore(jobRunFilePath, jobFilePath, isAbort); err != nil {
		// Centralized cleanup on ANY error
		if moveErr := agentutils.MoveMatchingToDir(filePattern, DataFolderPath, errorSubFolderPath, utils.MoveFileWithLock); moveErr != nil {
			logger.WriteLog("JAAGENTMNG200028", funcName, filePattern, DataFolderPath, errorSubFolderPath, moveErr)
		}
		if moveErr := agentutils.MoveFileToDir(jobFilePath, ErrorFolderPath, utils.MoveFileWithLock); moveErr != nil {
			logger.WriteLog("JAAGENTMNG200029", funcName, jobFilePath, ErrorFolderPath, moveErr)
		}

		return err
	}

	return nil
}

func createJobResultFileCore(jobRunFilePath, jobFilePath string, isAbort bool) error {

	funcName := "createJobResultFileCore"
	logger.WriteLog("JAAGENTMNG000001", funcName, jobRunFilePath, isAbort)

	var (
		matches, missing                             []string
		bytes                                        []byte
		endFilePath, baseFilePath, serverIP, content string
		pid, retCode                                 int
		eventData                                    *common.EventData
		jobResultData                                common.JobResultData
		err                                          error
	)

	_, err = os.Stat(jobRunFilePath)
	if err != nil {
		// json file no longer exists
		logger.WriteLog("JAAGENTMNG000011", funcName, jobFilePath, jobRunFilePath, err)
		if err := agentutils.MoveFileToDir(jobFilePath, ErrorFolderPath, utils.MoveFileWithLock); err != nil {
			return fmt.Errorf("failed to move job file [%s] to error folder [%s]: %v", jobFilePath, ErrorFolderPath, err)
		}
		return nil
	}

	pattern := strings.TrimSuffix(jobRunFilePath, ".json") + "-*.end"
	matches, err = filepath.Glob(pattern)
	if err != nil {
		return fmt.Errorf("glob error: %w", err)
	}

	base := filepath.Base(jobFilePath)
	baseFileName := strings.TrimSuffix(base, ".job")
	baseFilePath = filepath.Join(DataFolderPath, baseFileName)

	if len(matches) == 0 {
		missing, err = agentutils.CheckDataFiles(jobRunFilePath)
		if err != nil {
			return fmt.Errorf("failed to check data files [%s]: %v", jobRunFilePath, err)
		}

		if len(missing) > 0 {
			logger.WriteLog("JAAGENTMNG300004", funcName, baseFilePath, missing)
			// there are missing data files; create them if not exists
			if err = agentutils.CreateEmptyDataFilesCore(DataFolderPath, baseFileName); err != nil {
				return fmt.Errorf("failed to create data file: %v", err)
			}
		}

		endFilePath = baseFilePath + ".end"
	} else {
		// Extract baseName without extension
		endFilePath = matches[0]
		baseFilePath = strings.TrimSuffix(endFilePath, ".end")
	}

	// Add end time
	err = agentutils.WriteCurrentTimeToFile(endFilePath)
	if err != nil {
		return fmt.Errorf("failed to write current time to end file '%s': %v", endFilePath, err)
	}

	pid, err = agentutils.ExtractPIDFromFilename(endFilePath)
	if err != nil {
		return fmt.Errorf("failed to extract pid from filename '%s': %v", endFilePath, err)
	}

	// Create event file so that sender will send response to server
	bytes, err = os.ReadFile(jobRunFilePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %v", err)
	}

	eventData, _, err = agentutils.UnmarshalEventOrTCPMessage(bytes)
	if err != nil {
		return fmt.Errorf("failed to unmarshal event data [data=%s]: %v", string(bytes), err)
	}

	// Get runRequestData
	jobRunRequestData, ok := eventData.TCPMessage.Data.(common.JobRunRequestData)
	if !ok {
		return fmt.Errorf("failed to type cast to %T. raw_data=%s", jobRunRequestData, utils.ToReadableString(eventData.TCPMessage.Data))
	}

	// JobResult data preparation
	jobResultData.JobRunRequestData = jobRunRequestData

	agentutils.PrepareJobResultHeader(eventData, &jobResultData, pid)

	jobResultData.JobStatus = common.AgentJobStatusEnd
	jobResultData.Result = common.JA_JOBRESULT_SUCCEED

	serverIP, err = agentutils.GetIPByServerID(ServerIPsFilePath, eventData.TCPMessage.ServerID)
	if err != nil {
		return fmt.Errorf("failed to get server IP by server ID [%s]: %v", eventData.TCPMessage.ServerID, err)
	}

	tmpJobFilePath := filepath.Join(JobsFolderPath, fmt.Sprintf("%s_%s", jobResultData.JobID, strings.ReplaceAll(serverIP, ".", "-")+".job"))
	jobResultData.PreUniqueID, err = agentutils.ReadLastLineOrNew(tmpJobFilePath)
	if err != nil {
		return fmt.Errorf("failed to read or create new job file [%s]: %v", tmpJobFilePath, err)
	}

	jobResultData.CurUniqueID = fmt.Sprintf("%s_%s_%s", jobResultData.JobID, agentutils.GetTimestampWithNanoseconds(), strings.ReplaceAll(serverIP, ".", "-"))

	// Read stdout
	stdoutPath := baseFilePath + ".stdout"
	content, err = agentutils.ReadFileAsLocale(stdoutPath, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("error reading stdout file: %w", err)

	}

	jobResultData.StdOut = string(content)

	// Read stderr
	stderrPath := baseFilePath + ".stderr"
	content, err = agentutils.ReadFileAsLocale(stderrPath, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("error reading stdout file: %w", err)

	}

	jobResultData.StdErr = string(content)

	isIncompleteExec := true

	// Read return code
	retPath := baseFilePath + ".ret"
	content, err = agentutils.ReadFileAsLocale(retPath, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("error reading ret file: %w", err)

	}

	retCodeStr := strings.TrimSpace(string(content))
	if retCodeStr != "" {
		retCode, err = strconv.Atoi(strings.TrimSpace(retCodeStr))
		if err != nil {
			return fmt.Errorf("ret conversion error: %v", err)
		}
		isIncompleteExec = false
	}

	if jobResultData.Type == common.AgentJobTypeReboot {
		// Check if the reboot flag file exists
		rebootFlagFilePath := fmt.Sprintf("%s%s", RebootFlagFilePrefix, jobResultData.JobID)

		// Check if the reboot flag file exists
		if _, err = os.Stat(rebootFlagFilePath); err == nil {
			// File exists — but reboot process no longer exists
			logger.WriteLog("JAAGENTMNG400030", funcName, rebootFlagFilePath, jobResultData.JobID)
			isIncompleteExec = true
		} else if !errors.Is(err, os.ErrNotExist) {
			// Other unexpected error accessing the file
			logger.WriteLog("JAAGENTMNG200032", funcName, rebootFlagFilePath, jobResultData.JobID, err)
			jobResultData.Message = fmt.Sprintf("failed to check reboot flag file: %v. jobid: %s", err, jobResultData.JobID)
			retCode = -1
			jobResultData.ReturnCode = retCode
			jobResultData.Result = common.JA_JOBRESULT_FAIL
			isIncompleteExec = false
		} else {
			// file no longer exists. reboot job done successfully
			isIncompleteExec = false
			retCode = 137                // killed by SIGKILL (9) from shutdown
			jobResultData.ReturnCode = 0 // reboot success
		}

	}

	if isIncompleteExec {
		retCode = -1
		jobResultData.ReturnCode = retCode
		jobResultData.Message = fmt.Sprintf("incomplete job execution. jobid: %s", jobResultData.JobID)
		jobResultData.Result = common.JA_JOBRESULT_FAIL
	}

	if isAbort {
		jobResultData.Signal = 1
		jobResultData.JobStatus = common.AgentJobStatusAbort
		jobResultData.Result = common.JA_JOBRESULT_SUCCEED
		retCode = 9
		jobResultData.Message = ""
		jobResultData.ReturnCode = retCode
		jobResultData.Method = common.AgentMethodKill

		// Remove reboot flag file if it is a reboot job
		if jobResultData.Type == common.AgentJobTypeReboot {
			rebootFlagFilePath := fmt.Sprintf("%s%s", RebootFlagFilePrefix, jobResultData.JobID)
			if err = os.Remove(rebootFlagFilePath); err != nil {
				logger.WriteLog("JAAGENTMNG200006", funcName, rebootFlagFilePath, err)
			}
		}
	}

	// append end time and ret code
	if err = agentutils.WriteCurrentTimeToFile(jobFilePath); err != nil {
		return fmt.Errorf("failed to write current time to job file '%s': %v", endFilePath, err)
	}

	if err = agentutils.WriteStringToFile(jobFilePath, fmt.Sprintf("%d", retCode)); err != nil {
		return fmt.Errorf("failed to write ret code to job file '%s': %v", endFilePath, err)
	}

	eventData.TCPMessage.Data = jobResultData

	eventData.Transfer.Files = []common.FileTransfer{}

	if err = agentutils.CreateJobResultJsonFile(eventData, jobFilePath, jobRunFilePath, InFolderPath, EndFolderPath, jobResultData.JobID); err != nil {
		return fmt.Errorf("failed to create event file [%s]: %v", jobRunFilePath, err)
	}

	return nil
}

// Global variables
const (
	RebootScriptFileName = "jareboot.sh"
	JobRunClientExecName = "job_run_client"
	FCopyClientExecName  = "fcopy_client"
)

var RebootScriptFile string
var RebootFlagFilePrefix string
var TmpFolderPath string
var InFolderPath, TempFolderPath, DataFolderPath, ExecFolderPath, ErrorFolderPath, EndFolderPath, CloseFolderPath, AbortFolderPath, RunCountFolderPath, LockFolderPath string
var ServerIPsFilePath, JobsFolderPath string
var JobRunClientExecPath, FCopyClientExecPath string

func initValue() {
	RebootScriptFile = filepath.Join(agent.Options.JaExtjobPath, RebootScriptFileName)
	RebootFlagFilePrefix = filepath.Join(agent.Options.TmpDir, "jobarg_agentd_reboot_flag-")

	// Folder paths
	TmpFolderPath = filepath.Join(agent.Options.TmpDir, common.AgentManagerFolder)

	InFolderPath = filepath.Join(TmpFolderPath, common.InFolder)
	TempFolderPath = filepath.Join(TmpFolderPath, agentutils.TempFolder)
	DataFolderPath = filepath.Join(TmpFolderPath, agentutils.DataFolder)
	ExecFolderPath = filepath.Join(TmpFolderPath, agentutils.ExecFolder)
	ErrorFolderPath = filepath.Join(TmpFolderPath, agentutils.ErrorFolder)
	EndFolderPath = filepath.Join(TmpFolderPath, agentutils.EndFolder)
	CloseFolderPath = filepath.Join(TmpFolderPath, agentutils.CloseFolder)
	AbortFolderPath = filepath.Join(TmpFolderPath, agentutils.AbortFolder)
	ServerIPsFilePath = filepath.Join(TmpFolderPath, agentutils.ServerIPFolder, "serverIPs.json")
	JobsFolderPath = filepath.Join(TmpFolderPath, agentutils.JobFolder)
	RunCountFolderPath = filepath.Join(TmpFolderPath, "run_count")
	LockFolderPath = filepath.Join(TmpFolderPath, "lock")

	var ext, clientParentFolder string
	if runtime.GOOS == "windows" {
		ext = ".exe"
		execPath, err := os.Executable()
		if err != nil {
			os.Exit(1)
		}
		clientParentFolder = filepath.Dir(execPath)
	} else {
		clientParentFolder = "/usr/local/bin/jobarranger"
	}

	JobRunClientExecPath = filepath.Join(clientParentFolder, JobRunClientExecName+ext)
	FCopyClientExecPath = filepath.Join(clientParentFolder, FCopyClientExecName+ext)

}

func init() {
	initValue()
}

func ProcessEventData(data common.Data) {

	funcName := "ProcessEventData"
	logger.WriteLog("JAAGENTMNG400005", funcName)

	err := agentutils.EnsureAgentFoldersExist(TmpFolderPath)
	if err != nil {
		logger.WriteLog("JAAGENTMNG200010", funcName, err)
		return
	}

	eventData, jazVersion, err := agentutils.UnmarshalEventOrTCPMessage(data.EventData)
	if err != nil {
		logger.WriteLog("JAAGENTMNG200011", funcName, string(data.EventData), err)
		return
	}

	logger.WriteLog("JAAGENTMNG400031", funcName, eventData.Event.Name, jazVersion)

	// Validate the meta tags
	if err := validateTCPMetaTags(eventData.TCPMessage); err != nil {
		logger.WriteLog("JAAGENTMNG200012", funcName, err)
		return
	}

	// Set TCP timeouts
	if data.NetConn != nil {
		data.NetConn.SetReceiveTimeout(int64(agent.Options.Timeout))
		data.NetConn.SetSendTimeout(int64(agent.Options.Timeout))
	}

	// TCP requests from servers
	if eventData.TCPMessage.Kind != common.KindJobResult {
		// Get serverIP
		serverIP, err := agentutils.GetConnectionIP(data.NetConn)
		if err != nil {
			logger.WriteLog("JAAGENTMNG200013", funcName, err)
			return
		}

		// validate allowed hostIPs
		if agent.Options.Server != "" && !agentutils.IsInAllowedList(serverIP, agent.Options.Server) {
			logger.WriteLog("JAAGENTMNG200014", funcName, serverIP, agent.Options.Server)
			return
		}

		logger.WriteLog("JAAGENTMNG400007", funcName, serverIP, agent.Options.Server)

		// Update serverID
		if err := agentutils.UpdateOrAppendJSON(ServerIPsFilePath, eventData.TCPMessage.ServerID, serverIP); err != nil {
			logger.WriteLog("JAAGENTMNG200015", funcName, ServerIPsFilePath, err)
			return
		}

		// Prepare TCP data for jaz 2
		if jazVersion == 2 {
			logger.WriteLog("JAAGENTMNG400001", funcName, eventData.Event.Name, eventData.TCPMessage.Kind, *eventData.TCPMessage.JazVersion, eventData.TCPMessage.Hostname, eventData.TCPMessage.Version, eventData.TCPMessage.ServerID)

			if err := agentutils.PrepareTCPDataFromEventData(eventData); err != nil {
				logger.WriteLog("JAAGENTMNG200001", funcName, err.Error())

				errMsg := fmt.Sprintf("invalid tcp data: %v", err)
				eventData.TCPMessage.Data = common.ResponseData{
					Message: &errMsg,
					Result:  common.JA_JOBRESULT_FAIL,
				}

				if err := agentutils.SendToServer(eventData, data.NetConn); err != nil {
					logger.WriteLog("JAAGENTMNG200016", funcName, err)
					return
				}
				return
			}

		}
	}

	logger.WriteLog("JAAGENTMNG400012", funcName, eventData.Event.Name, eventData.TCPMessage.Kind, *eventData.TCPMessage.JazVersion, eventData.TCPMessage.Hostname, eventData.TCPMessage.Version, eventData.TCPMessage.ServerID)

	// Handle kinds
	switch eventData.TCPMessage.Kind {
	case common.KindJobRun:
		err = handleAgentJobRun(eventData, data.NetConn)
	case common.KindFileCopy:
		err = processAgentFileCopy(eventData, data.NetConn)
	case common.KindJobResult:
		err = handleAgentJobResult(eventData)
	case common.KindCheckJob:
		err = handleAgentCheckJob(eventData, data.NetConn)
	case common.KindIPChange:
		err = handleAgentIPChange(eventData, data.NetConn)
	default:
		logger.WriteLog("JAAGENTMNG200017", funcName, eventData.TCPMessage.Kind)
		return
	}

	if err != nil {
		logger.WriteLog("JAAGENTMNG200021", funcName, eventData.TCPMessage.Kind, err)
	}

}

func processAgentFileCopy(eventData *common.EventData, netConn *common.NetConnection) error {
	fn := "processAgentFileCopy"

	// file copy kind can be for check-version or fcopy request
	tcpData, ok := eventData.TCPMessage.Data.(common.JobRunRequestData)
	if !ok {
		return fmt.Errorf("failed to type cast to %T. raw_data=%s", tcpData, utils.ToReadableString(eventData.TCPMessage.Data))
	}

	eventData.Event.Name = common.EventAgentJobResult

	logger.WriteLog("JAAGENTMNG400033", fn, tcpData.JobID, tcpData.Type)

	switch tcpData.Type {
	case common.AgentJobTypeCheckVersion:
		err := handleAgentCheckVersion(eventData, netConn)
		if err != nil {
			return fmt.Errorf("failed to handle check version for jobid [%s]: %v", tcpData.JobID, err)
		}
	case common.AgentJobTypePutFile, common.AgentJobTypeGetFile:
		err := handleAgentFileCopy(eventData, netConn)
		if err != nil {
			return fmt.Errorf("failed to handle file copy for jobid [%s]: %v", tcpData.JobID, err)
		}
	default:
		return fmt.Errorf("invalid job type for jobid [%s]: %s", tcpData.JobID, tcpData.Type)
	}

	return nil
}

func handleAgentCheckVersion(eventData *common.EventData, netConn *common.NetConnection) error {
	eventData.TCPMessage.Kind = common.KindFileCopyRes
	eventData.TCPMessage.EncryptVersion = common.EncryptVersion
	eventData.TCPMessage.Data = common.ResponseData{
		Result: common.JA_RESPONSE_SUCCEED,
	}

	return agentutils.SendToServer(eventData, netConn)
}

func handleAgentJobRun(eventData *common.EventData, netConn *common.NetConnection) error {
	funcName := "handleAgentJobRun"
	var jobRunData common.IconExecutionProcessData

	tcpData, ok := eventData.TCPMessage.Data.(common.JobRunRequestData)
	if !ok {
		return fmt.Errorf("failed to type cast to %T. raw_data=%s", tcpData, utils.ToReadableString(eventData.TCPMessage.Data))
	}

	if eventData.TCPMessage.JazVersion != nil && *eventData.TCPMessage.JazVersion == 2 {
		jobRunData, ok = eventData.NextProcess.Data.(common.IconExecutionProcessData)
		if !ok {
			return fmt.Errorf("failed to type cast to %T. raw_data=%s", jobRunData, utils.ToReadableString(eventData.NextProcess.Data))
		}
	}

	logger.WriteLog("JAAGENTMNG000005", funcName, tcpData.JobID, tcpData.Method, tcpData.Type, eventData.TCPMessage.Kind, eventData.TCPMessage.Hostname, eventData.TCPMessage.Version, eventData.TCPMessage.ServerID)

	// Check for abort process
	if tcpData.Method == common.AgentMethodKill {
		handleJobAbort(eventData, &tcpData, netConn)
		return nil
	}

	jobID, err := strconv.ParseUint(tcpData.JobID, 10, 64)
	if err != nil {
		return fmt.Errorf("failed to parse jobid [%s]: %v", tcpData.JobID, err)
	}

	// Cancel duplicate
	isAlreadyRunning, err := isJobAlreadyRunning(tcpData, eventData.TCPMessage.ServerID)
	if err != nil {
		errMsg := fmt.Sprintf("failed to check if job is already running for jobid [%s]: %s", tcpData.JobID, err.Error())
		logger.WriteLog("JAAGENTMNG200018", funcName, tcpData.JobID, err.Error())
		eventData.TCPMessage.Data = common.ResponseData{
			Result:  common.JA_JOBRESULT_FAIL,
			Message: &errMsg,
		}

		return agentutils.SendToServer(eventData, netConn)
	}

	if isAlreadyRunning {
		msg := fmt.Sprintf("Job with same job id [%s] is already running.", tcpData.JobID)
		eventData.TCPMessage.Data = common.ResponseData{
			Result:  common.JA_JOBRESULT_SUCCEED,
			Message: &msg,
		}

		return agentutils.SendToServer(eventData, netConn)
	}

	if eventData.TCPMessage.JazVersion != nil && *eventData.TCPMessage.JazVersion == 2 {
		flag, err := event.CheckRunCountFileAgent(RunCountFolderPath, eventData.TCPMessage.ServerID, jobID, jobRunData.RunJobData.RunCount, int(jobRunData.RunJobData.MethodFlag))
		if err != nil {
			msg := fmt.Sprintf("failed to create flag file. err: %v", err)

			eventData.TCPMessage.Data = common.ResponseData{
				Result:  common.JA_JOBRESULT_SUCCEED,
				Message: &msg,
			}

			return agentutils.SendToServer(eventData, netConn)
		}
		if !flag {
			logger.WriteLog("JAAGENTMNG000006", funcName, tcpData.JobID)

			eventData.TCPMessage.Data = common.ResponseData{
				Result: common.JA_JOBRESULT_SUCCEED,
			}

			return agentutils.SendToServer(eventData, netConn)
		}
	}

	eventData.Transfer.Files = []common.FileTransfer{}
	eventData.Event.Name = common.EventAgentJobRun
	currentTimestamp := agentutils.GetTimestampWithNanoseconds()
	if err := agentutils.CreateJsonFileAgent(eventData, strconv.FormatUint(jobID, 10), DataFolderPath, currentTimestamp); err != nil {
		errMsg := fmt.Sprintf("failed to create event file for jobid [%s]: %v", tcpData.JobID, err)
		logger.WriteLog("JAAGENTMNG200020", funcName, tcpData.JobID, err)
		eventData.TCPMessage.Data = common.ResponseData{
			Result:  common.JA_JOBRESULT_FAIL,
			Message: &errMsg,
		}

		return agentutils.SendToServer(eventData, netConn)
	}

	if len(eventData.Transfer.Files) == 0 || eventData.Transfer.Files[0].Source == "" {
		return fmt.Errorf("event source file path is missing for jobid [%s]", tcpData.JobID)
	}

	logger.WriteLog("JAAGENTMNG400015", funcName, eventData.Transfer.Files[0].Source, tcpData.JobID)

	if eventData.TCPMessage.JazVersion != nil && *eventData.TCPMessage.JazVersion == 2 {
		if err := event.CreateRunCountFileAgent(RunCountFolderPath, eventData.TCPMessage.ServerID, jobID, jobRunData.RunJobData.RunCount); err != nil {
			return fmt.Errorf("failed to create run count file for jobid [%d]: %v", jobID, err)
		}
	}

	transacFile := eventData.Transfer.Files[0].Source
	cmd, err := startJobRunProcess(tcpData, transacFile)
	if err != nil {
		logger.WriteLog("JAAGENTMNG200019", funcName, tcpData.JobID, err)
		msg := fmt.Sprintf("failed to start the job run process for jobid [%s]: %s", tcpData.JobID, err.Error())
		eventData.TCPMessage.Data = common.ResponseData{
			Result:  common.JA_JOBRESULT_FAIL,
			Message: &msg,
		}

		// clean up transaction file as the job has failed
		if err := os.Remove(transacFile); err != nil {
			logger.WriteLog("JAAGENTMNG200037", funcName, transacFile, tcpData.JobID, err)
		}

		return agentutils.SendToServer(eventData, netConn)
	}

	logger.WriteLog("JAAGENTMNG400029", funcName, cmd.Process.Pid)

	// Send acknowledge for jobrun-res
	eventData.Event.Name = common.EventAgentJobRunResp
	eventData.TCPMessage.Kind = common.KindJobRunRes
	eventData.TCPMessage.Data = common.ResponseData{
		Result: common.JA_JOBRESULT_SUCCEED,
	}

	return agentutils.SendToServer(eventData, netConn)

}

func isJobAlreadyRunning(tcpData common.JobRunRequestData, serverID string) (bool, error) {
	fn := "isJobAlreadyRunning"
	logger.WriteLog("JAAGENTMNG400034", fn, tcpData.JobID, serverID)

	// check if file exists with jobid
	matches, err := agentutils.FindMatchingFiles(DataFolderPath, fmt.Sprintf("%s-*.json", tcpData.JobID))
	if err != nil {
		return false, fmt.Errorf("failed to find transaction file in folder '%s': %v", DataFolderPath, err)
	}

	if len(matches) <= 0 {
		logger.WriteLog("JAAGENTMNG400035", fn, tcpData.JobID)
		return false, nil
	}

	logger.WriteLog("JAAGENTMNG400036", fn, tcpData.JobID, matches)

	for _, foundFile := range matches {
		logger.WriteLog("JAAGENTMNG400037", fn, tcpData.JobID, foundFile)

		// check server id
		bytes, err := os.ReadFile(foundFile)
		if err != nil {
			return true, fmt.Errorf("failed to read file '%s': %v", foundFile, err)
		}

		var eventData common.EventData
		if err := json.Unmarshal(bytes, &eventData); err != nil {
			return true, fmt.Errorf("failed to unmarshal the content of file '%s': %v [content=%s]", foundFile, err, string(bytes))
		}

		if eventData.TCPMessage.ServerID == serverID {
			// duplicated execution
			logger.WriteLog("JAAGENTMNG400038", fn, tcpData.JobID, serverID, foundFile)
			return true, nil
		}
	}

	return false, nil
}

func handleJobAbort(eventData *common.EventData, tcpData *common.JobRunRequestData, netConn *common.NetConnection) {
	funcName := "handleJobAbort"

	logger.WriteLog("JAAGENTMNG400013", funcName, tcpData.JobID)

	// Checking the abort flag if it already exists
	abortFlagExists, err := checkJobAbortFlag(tcpData.JobID)
	if err != nil {
		logger.WriteLog("JAAGENTMNG200008", funcName, tcpData.JobID, err)
		return
	}

	// Already exists
	if abortFlagExists {
		logger.WriteLog("JAAGENTMNG000002", funcName, tcpData.JobID)
	}

	// Prepare the acknowledge
	eventData.Event.Name = common.EventAgentJobRunResp
	eventData.TCPMessage.Kind = common.KindJobRunRes
	eventData.TCPMessage.Data = common.ResponseData{
		Result: common.JA_JOBRESULT_SUCCEED,
	}

	// send tcp back to server
	if err := agentutils.SendToServer(eventData, netConn); err != nil {
		logger.WriteLog("JAAGENTMNG200009", funcName, tcpData.JobID, err)
	}

	logger.WriteLog("JAAGENTMNG400014", funcName, tcpData.JobID)

}

func checkJobAbortFlag(jobID string) (bool, error) {
	fn := "checkJobAbortFlag"
	logger.WriteLog("JAAGENTMNG400004", fn, jobID)

	if err := agentutils.CreateFolderIfNotExist(AbortFolderPath); err != nil {
		return false, fmt.Errorf("failed to create folder: %s", AbortFolderPath)
	}

	abortFlagFilePath := filepath.Join(AbortFolderPath, jobID)
	if agentutils.FileExists(abortFlagFilePath) {
		// File exists
		return true, nil
	}

	shouldCreateFlagFile := false

	// find matching job file for the abort jobid
	searchPattern := fmt.Sprintf("%s-*.job", jobID)
	matches, err := agentutils.FindMatchingFiles(ExecFolderPath, searchPattern)
	if err != nil {
		return false, fmt.Errorf("failed to find job file in folder '%s': %v", ExecFolderPath, err)
	}

	if len(matches) > 0 {
		logger.WriteLog("JAAGENTMNG400002", fn, matches[0], ExecFolderPath, jobID)
		shouldCreateFlagFile = true
	} else {
		// Search in close folder
		matches, err = agentutils.FindMatchingFiles(CloseFolderPath, searchPattern)
		if err != nil {
			return false, fmt.Errorf("failed to find job file in folder '%s': %v", CloseFolderPath, err)
		}

		if len(matches) > 0 {
			logger.WriteLog("JAAGENTMNG400003", fn, matches[0], CloseFolderPath, jobID)
			return false, nil
		}
	}

	if shouldCreateFlagFile {
		if err := agentutils.CreateFile(abortFlagFilePath); err != nil {
			return false, fmt.Errorf("failed to create abort flag file: %v", err)
		}

		logger.WriteLog("JAAGENTMNG000003", fn, abortFlagFilePath, jobID)
	}

	return false, nil

}

func handleAgentJobResult(eventData *common.EventData) error {
	funcName := "handleAgentJobResult"

	// Type cast to ensure proper data is received for this event
	var jobResultData common.JobResultData
	err := utils.Convert(eventData.TCPMessage.Data, &jobResultData)
	if err != nil {
		return fmt.Errorf("failed to convert to %T. raw_data [%s]: %v", jobResultData, utils.ToReadableString(eventData.TCPMessage.Data), err)
	}

	if len(eventData.Transfer.Files) == 0 || eventData.Transfer.Files[0].Source == "" {
		return fmt.Errorf("event source file path is missing for jobid [%s]", jobResultData.JobID)
	}

	transacFile := eventData.Transfer.Files[0].Source
	baseFilename := filepath.Base(strings.TrimSuffix(transacFile, ".json"))
	filePattern := baseFilename + "*"

	pidStr := fmt.Sprintf("%d", jobResultData.PID)
	if jobResultData.PID == 0 {
		pidStr = "0000"
	}

	closeSubFolder := filepath.Join(CloseFolderPath, fmt.Sprintf("%s-%s", baseFilename, pidStr))

	jobFilePath := filepath.Join(EndFolderPath, fmt.Sprintf("%s-%s.job", baseFilename, pidStr))

	for {

		err := sendJobResult(eventData, &jobResultData)
		if err == nil {
			break
		}
		if _, statErr := os.Stat(transacFile); os.IsNotExist(statErr) {
			logger.WriteLog("JAAGENTMNG300006", funcName, jobResultData.JobID, transacFile, statErr)
			return nil // stop retrying
		}
		eventData.TCPMessage.Data = jobResultData

		var sendRetry int
		if eventData.TCPMessage.SendRetry != nil {
			sendRetry = *eventData.TCPMessage.SendRetry
		}

		logger.WriteLog("JAAGENTMNG200024", funcName, jobResultData.JobID, sendRetry, err)
		sendRetry += 1
		eventData.TCPMessage.SendRetry = &sendRetry

		if err := agentutils.WriteToFile(transacFile, eventData); err != nil {
			logger.WriteLog("JAAGENTMNG200025", funcName, transacFile, err)
		}

		time.Sleep(3 * time.Second)
	}

	// Move .json to data
	err = agentutils.MoveFileToDir(transacFile, DataFolderPath, utils.MoveFileWithLock)
	if err != nil {
		return fmt.Errorf("failed to move event file [%s] to dir [%s]: %v", transacFile, DataFolderPath, err)
	}
	logger.WriteLog("JAAGENTMNG400018", funcName, transacFile, DataFolderPath)

	// Move data files to close folder
	if err := agentutils.MoveMatchingToDir(filePattern, DataFolderPath, closeSubFolder, utils.MoveFileWithLock); err != nil {
		return fmt.Errorf("failed to move the matching files [%s] from dir [%s] to dir [%s]: %v", filePattern, DataFolderPath, closeSubFolder, err)
	}
	logger.WriteLog("JAAGENTMNG400020", funcName, filePattern, DataFolderPath, closeSubFolder)

	// Move .job to close
	if err := agentutils.MoveFileToDir(jobFilePath, CloseFolderPath, utils.MoveFileWithLock); err != nil {
		return fmt.Errorf("failed to move job file [%s] to dir [%s]: %v", jobFilePath, CloseFolderPath, err)
	}
	logger.WriteLog("JAAGENTMNG400018", funcName, jobFilePath, CloseFolderPath)

	return nil
}

func handleAgentIPChange(eventData *common.EventData, netConn *common.NetConnection) error {
	funcName := "handleAgentIPChange"

	connIP, err := agentutils.GetConnectionIP(netConn)
	if err != nil {
		return fmt.Errorf("failed to get connection ip: %v", err)
	}

	// Read serverIPs.json file
	serverIP, err := agentutils.GetIPByServerID(ServerIPsFilePath, eventData.TCPMessage.ServerID)
	returnCode := -1
	if err == nil {
		returnCode = 0
		if connIP == serverIP {
			// No need to update the file
			logger.WriteLog("JAAGENTMNG400028", funcName, serverIP, connIP)
		} else {
			// Overwrite the value
			if err := agentutils.UpdateOrAppendJSON(ServerIPsFilePath, eventData.TCPMessage.ServerID, connIP); err != nil {
				return fmt.Errorf("failed to update or append serverIPs file [%s]: %v", ServerIPsFilePath, err)
			}
		}

	} else {
		logger.WriteLog("JAAGENTMNG200031", funcName, eventData.TCPMessage.ServerID, err)
		if errors.Is(err, agentutils.ErrServerIDNotExist) {
			returnCode = -1
		} else {
			// failed to read, create a new one
			if err := agentutils.UpdateOrAppendJSON(ServerIPsFilePath, eventData.TCPMessage.ServerID, connIP); err != nil {
				return fmt.Errorf("failed to update or append serverIPs file [%s]: %v", ServerIPsFilePath, err)
			}
			returnCode = 0
		}
	}

	eventData.TCPMessage.Data = common.JobResultData{
		ReturnCode: returnCode,
	}

	// Send to server
	return agentutils.SendToServer(eventData, netConn)
}

func handleAgentCheckJob(eventData *common.EventData, netConn *common.NetConnection) error {
	funcName := "handleAgentCheckJob"
	tcpMsg := eventData.TCPMessage

	// Iterate the host_list [1]
	var jobResultData common.JobResultData
	err := utils.Convert(eventData.TCPMessage.Data, &jobResultData)
	if err != nil {
		return fmt.Errorf("failed to convert to %T. raw_data [%s]: %v", jobResultData, utils.ToReadableString(eventData.TCPMessage.Data), err)
	}

	jobIDList := jobResultData.HostJobList
	logger.WriteLog("JAAGENTMNG400025", funcName, tcpMsg.Kind, tcpMsg.Hostname, tcpMsg.Version, tcpMsg.ServerID, jobIDList)

	folders := []string{"temp", "exec", "end", "error", "close"}
	baseDir := filepath.Join(agent.Options.TmpDir, common.AgentManagerFolder)

	// Acquire an exclusive lock on the lock file before check process start.
	file, err := utils.LockFile(utils.LOCKFILE_EXCLUSIVE_LOCK | utils.LOCKFILE_FAIL_ON_LOCK)
	if err != nil {
		return fmt.Errorf("In %s(), failed to acquire exclusive lock. err: [%v]", funcName, err)
	}

	defer func() {
		// Unlock the lock file
		err = utils.UnlockFile(file)
		if err != nil {
			logger.JaLog("JAUTILS200002", logger.Logging{}, funcName, common.Manager.Name, "", "", err)
		}
	}()

	for _, jobIDInt := range jobIDList {
		logger.WriteLog("JAAGENTMNG400022", funcName, jobIDInt)
		var foundPath string
		jobID := strconv.FormatUint(jobIDInt, 10)

		// Iterate all folders
		for _, folder := range folders {
			// Build the glob pattern, e.g., /base/folder/72*.job
			pattern := filepath.Join(baseDir, folder, jobID+"-*.job")
			matches, err := filepath.Glob(pattern)
			if err != nil {
				logger.WriteLog("JAAGENTMNG200026", funcName, pattern, folder, err)
				continue
			}

			// If matching files are found, use the last one
			if len(matches) > 0 {
				foundPath = matches[len(matches)-1]
				logger.WriteLog("JAAGENTMNG400023", funcName, jobID, folder, foundPath)
				break
			}
		}

		if foundPath == "" {
			// no transaction file found with that jobid
			logger.WriteLog("JAAGENTMNG300003", funcName, jobID, folders)
			if err := handleJobFileMissing(eventData, jobID); err != nil {
				logger.WriteLog("JAAGENTMNG200027", funcName, jobID, err.Error())
			}
			continue
		}

		// Determine current folder
		foundDir := filepath.Dir(foundPath)
		currentFolder := filepath.Base(foundDir)
		if currentFolder == "temp" || currentFolder == "exec" || currentFolder == "end" {
			// Job is still running, skip
			logger.WriteLog("JAAGENTMNG400024", funcName, jobID, currentFolder)
			continue
		}

		if currentFolder == "error" || currentFolder == "close" {
			logger.WriteLog("JAAGENTMNG400039", funcName, jobID, currentFolder)

			// Job file pattern
			// Move data files to data folder
			foundDataFolderPath := strings.TrimSuffix(foundPath, ".job")
			filePattern := filepath.Base(foundDataFolderPath + "*")
			if err := agentutils.MoveMatchingToDir(filePattern, foundDataFolderPath, DataFolderPath, utils.MoveFileWithoutLock); err != nil {
				logger.WriteLog("JAAGENTMNG200028", funcName, filePattern, foundDataFolderPath, DataFolderPath, err)
				continue
			}

			// Move .job file to end folder
			if err := agentutils.MoveFileToDir(foundPath, EndFolderPath, utils.MoveFileWithoutLock); err != nil {
				logger.WriteLog("JAAGENTMNG200029", funcName, foundPath, EndFolderPath, err)
				continue
			}

			jsonFileName := filepath.Base(strings.TrimSuffix(foundPath[:strings.LastIndex(foundPath, "-")], ".job") + ".json")
			jsonPath := filepath.Join(foundDataFolderPath, jsonFileName)

			// add check_job_flag
			bytes, err := os.ReadFile(jsonPath)
			if err != nil {
				logger.WriteLog("JAAGENTMNG200034", funcName, jsonPath, err)
				continue
			}

			var eventData *common.EventData
			eventData, _, err = agentutils.UnmarshalEventOrTCPMessage(bytes)
			if err != nil {
				logger.WriteLog("JAAGENTMNG200011", funcName, string(bytes), err)
				continue
			}

			jobResultData, ok := eventData.TCPMessage.Data.(common.JobResultData)
			if !ok {
				logger.WriteLog("JAAGENTMNG200033", funcName, jobResultData, utils.ToReadableString(eventData.TCPMessage.Data))
				continue
			}

			jobResultData.CheckJobFlag = true
			eventData.TCPMessage.Data = jobResultData

			// Source file overwrite
			sourceFilePath := filepath.Join(InFolderPath, jsonFileName)
			eventData.Transfer.Files = []common.FileTransfer{
				{
					Source: sourceFilePath,
				},
			}

			if err := agentutils.WriteToFile(jsonPath, eventData); err != nil {
				logger.WriteLog("JAAGENTMNG200025", funcName, jsonPath, err)
				continue
			}

			if err := agentutils.MoveFileToDir(jsonPath, InFolderPath, utils.MoveFileWithoutLock); err != nil {
				logger.WriteLog("JAAGENTMNG200029", funcName, jsonPath, InFolderPath, err)
				continue
			}

			if err := os.Remove(foundDataFolderPath); err != nil {
				logger.WriteLog("JAAGENTMNG200030", funcName, foundDataFolderPath, err)
				continue
			}

			// Trigger jobresult send process, maybe enqueue or notify
			logger.WriteLog("JAAGENTMNG400026", funcName, jobID)
		}

	}

	if err := agentutils.SendToServer(eventData, netConn); err != nil {
		logger.WriteLog("JAAGENTMNG200016", funcName, err)
	} else {
		logger.WriteLog("JAAGENTMNG400027", funcName)
	}

	return nil
}

// Creates job and data files to resend the job result back to server with the error that .job file gone missing
func handleJobFileMissing(eventData *common.EventData, jobID string) error {
	fn := "handleJobFileMissing"

	// recreate data and job files to send error job result
	currentTimestamp := agentutils.GetTimestampWithNanoseconds()
	pid := "0000" // dummy pid

	// create .job file in end/ folder
	jobFilePath := filepath.Join(EndFolderPath, fmt.Sprintf("%s-%s-%s.job", jobID, currentTimestamp, pid))
	if err := agentutils.CreateFile(jobFilePath); err != nil {
		return fmt.Errorf("in %s(), failed to create job file '%s': %v", fn, jobFilePath, err)
	}

	// create data files [start, end, ret, sh, stdout, stderr] in data/ folder
	if err := agentutils.CreateEmptyDataFiles(DataFolderPath, jobID, currentTimestamp, pid); err != nil {
		return fmt.Errorf("in %s(), failed to create empty data files: %v", fn, err)
	}

	// prepare event data file
	var jobResultData common.JobResultData
	agentutils.PrepareJobResultHeader(eventData, &jobResultData, 0)
	jobResultData.Message = fmt.Sprintf("Files not found or corrupted.Error execution. jobid: %s", jobID)
	jobResultData.Result = common.JA_JOBRESULT_FAIL
	jobResultData.ReturnCode = -1
	jobResultData.Signal = 1
	jobResultData.JobID = jobID

	abortFlagFilePath := filepath.Join(AbortFolderPath, jobID)
	if agentutils.FileExists(abortFlagFilePath) {
		jobResultData.Method = common.AgentMethodKill
	}

	// Get pre unique and cur unique ids
	serverIP, err := agentutils.GetIPByServerID(ServerIPsFilePath, eventData.TCPMessage.ServerID)
	if err != nil {
		return fmt.Errorf("failed to get server IP by server ID [%s]: %v", eventData.TCPMessage.ServerID, err)
	}

	tmpJobFilePath := filepath.Join(JobsFolderPath, fmt.Sprintf("%s_%s", jobResultData.JobID, strings.ReplaceAll(serverIP, ".", "-")+".job"))
	jobResultData.PreUniqueID, err = agentutils.ReadLastLineOrNew(tmpJobFilePath)
	if err != nil {
		return fmt.Errorf("error reading job file: %w", err)
	}
	jobResultData.CurUniqueID = fmt.Sprintf("%s_%s_%s", jobResultData.JobID, agentutils.GetTimestampWithNanoseconds(), strings.ReplaceAll(serverIP, ".", "-"))

	eventData.TCPMessage.Data = jobResultData
	eventData.NextProcess.Data = common.IconExecutionProcessData{}

	// create .json file in in/ folder
	if err := agentutils.CreateJsonFileAgent(eventData, jobID, InFolderPath, currentTimestamp); err != nil {
		return fmt.Errorf("failed to create event file for job result of jobid [%s]: %v", jobID, err)
	}

	return nil
}

func sendJobResult(eventData *common.EventData, jobResultData *common.JobResultData) error {
	funcName := "sendJobResult"

	if jobResultData.Method == common.AgentMethodAbort {
		logger.WriteLog("JAAGENTMNG000008", funcName, jobResultData.JobID, jobResultData.Method)
		return nil
	}

	serverIP, err := agentutils.GetIPByServerID(ServerIPsFilePath, eventData.TCPMessage.ServerID)
	if err != nil {
		return fmt.Errorf("failed to get server IP by server ID [%s]: %v", eventData.TCPMessage.ServerID, err)
	}

	tcpClient := jatcp.CreateTcpClient(serverIP, agent.Options.JaServerPort, uint(agent.Options.Timeout), "")
	conn, err := tcpClient.Connect()
	if err != nil {
		return fmt.Errorf("failed to connect to server [%s:%d]: %v", serverIP, agent.Options.JaServerPort, err)
	}
	defer conn.Close()
	conn.SetReceiveTimeout(int64(agent.Options.Timeout))
	conn.SetSendTimeout(int64(agent.Options.Timeout))

	// Send jobresult
	if err := agentutils.SendToServer(eventData, conn); err != nil {
		return fmt.Errorf("failed to send job result: %v", err)
	}

	var response map[string]any
	if response, err = conn.Receive(); err != nil {
		return fmt.Errorf("failed to receive job result response: %v", err)
	}

	logger.WriteLog("JAAGENTMNG400032", funcName, response)

	respBytes, err := json.Marshal(response)
	if err != nil {
		return fmt.Errorf("failed to marshal job result response: %v", err)
	}

	respEventData, _, err := agentutils.UnmarshalEventOrTCPMessage(respBytes)
	if err != nil {
		return fmt.Errorf("failed to unmarshal jobresult response [data=%s]: %v", string(respBytes), err)
	}

	var respData common.ResponseData
	err = utils.Convert(respEventData.TCPMessage.Data, &respData)
	if err != nil {
		return fmt.Errorf("failed to convert to %T. raw_data [%s]: %v", respData, utils.ToReadableString(respEventData.TCPMessage.Data), err)
	}

	tmpJobFilePath := filepath.Join(JobsFolderPath, fmt.Sprintf("%s_%s", jobResultData.JobID, strings.ReplaceAll(serverIP, ".", "-")+".job"))
	if err := agentutils.CreateFolderIfNotExist(JobsFolderPath); err != nil {
		return fmt.Errorf("failed to create dir [%s]: %v", JobsFolderPath, err)
	}

	// Check the response message of jobresult
	if respData.Result == common.JobResultRespStatusFailed {
		var message, preUniqueID string

		if respData.PreUniqueID != nil {
			preUniqueID = *respData.PreUniqueID
		}

		if respData.Message != nil {
			message = *respData.Message
		}

		switch message {
		case common.DataRecovery:
			jobResultData.PreUniqueID = preUniqueID
			err := agentutils.AddUIDToJobFile(tmpJobFilePath, preUniqueID)
			if err != nil {
				logger.WriteLog("JAAGENTMNG200023", funcName, preUniqueID, tmpJobFilePath, err)
			}
			return fmt.Errorf("job result response is not ok: result [%d], message [%s]", respData.Result, message)
		case common.DuplicateData:
			jobResultData.PreUniqueID = preUniqueID
			err := agentutils.AddUIDToJobFile(tmpJobFilePath, preUniqueID)
			if err != nil {
				logger.WriteLog("JAAGENTMNG200023", funcName, preUniqueID, tmpJobFilePath, err)
			}
			logger.WriteLog("JAAGENTMNG000009", funcName, jobResultData.JobID, respData.Result, message, preUniqueID)
			if jobResultData.CheckJobFlag {
				return fmt.Errorf("job result response is not ok: result [%d], message [%s]", respData.Result, message)
			}
		case common.AfterStatusError:
			return fmt.Errorf("job result response is not ok: result [%d], message [%s]", respData.Result, message)
		default:
			if eventData.TCPMessage.JazVersion != nil && *eventData.TCPMessage.JazVersion == 2 {
				return fmt.Errorf("job result response is not ok: result [%d], message [%s]", respData.Result, message)
			}
		}
	}

	logger.WriteLog("JAAGENTMNG400021", funcName, respData.Result)

	// Write to tmpJobFile
	if err := agentutils.AddUIDToJobFile(tmpJobFilePath, jobResultData.CurUniqueID); err != nil {
		return fmt.Errorf("failed to add UID [%s] to tmp job file [%s]: %v", jobResultData.CurUniqueID, tmpJobFilePath, err)
	}

	logger.WriteLog("JAAGENTMNG000007", funcName, jobResultData.JobID)

	return nil
}

func startJobRunProcess(tcpData common.JobRunRequestData, jobRunFilePath string) (*exec.Cmd, error) {
	// job method
	switch tcpData.Method {
	case common.AgentMethodNormal, common.AgentMethodTest, common.AgentMethodAbort:
		// check job type
		switch tcpData.Type {
		case common.AgentJobTypeReboot:
			// check for allowroot
			if agent.Options.AllowRoot == 0 {
				// do not allow to run reboot
				return nil, fmt.Errorf("reboot job failed.[CONFIG_ALLOW_ROOT = 0]")
			}
		}

	default:
		return nil, fmt.Errorf("invalid method: %d", tcpData.Method)
	}

	proc := forker.New(forker.ProcessData{
		ExecPath: JobRunClientExecPath,
		ExecParams: []string{
			"-config-file-path", common.ConfigFilePath,
			"-job-run-file-path", jobRunFilePath,
		},

		DirectExec: true,
		Detached:   true,
	})

	return proc.StartNewProcess()
}

func validateTCPMetaTags(msg *common.TCPMessage) error {
	fn := "validateTCPMetaTags"

	if msg == nil {
		return fmt.Errorf("tcp message is nil")
	}

	// Log full JSON content
	raw, err := json.Marshal(msg)
	if err != nil {
		return fmt.Errorf("failed to marshal raw string: %v", err)
	}
	logger.WriteLog("JAAGENTMNG400006", fn, string(raw))

	// KIND
	if msg.Kind == "" {
		return fmt.Errorf("missing required field: %s", "kind")
	}

	// SERVERID
	if msg.ServerID == "" {
		return fmt.Errorf("missing required field: %s", "serverid")
	}

	// VERSION
	if msg.Version == 0 {
		return fmt.Errorf("missing or zero field: %s", "version")
	}

	// Version check
	var expectedVersion float64
	if msg.Kind == common.KindIPChange {
		expectedVersion = 1.3
	} else {
		expectedVersion = 1
	}
	if msg.Version != expectedVersion {
		return fmt.Errorf("version: [%.1f] does not match expected [%.1f]", msg.Version, expectedVersion)
	}

	// HOSTNAME
	if msg.Hostname == "" && agent.Options.Hostname != "" {
		msg.Hostname = agent.Options.Hostname
	}
	if agent.Options.Hostname != "" && !agentutils.IsInAllowedList(msg.Hostname, agent.Options.Hostname) {
		return fmt.Errorf("hostname '%s' does not match configured hostnames [%s]", msg.Hostname, agent.Options.Hostname)
	}

	// For kind == ipchange, skip data validation
	if msg.Kind == common.KindIPChange {
		return nil
	}

	// DATA presence
	if msg.Data == nil {
		return fmt.Errorf("missing required field: %s", "data")
	}

	return nil
}

func parseArgs() (args, *flag.FlagSet) {
	var args args
	flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)

	flagSet.BoolVar(&args.Daemon, "daemon", false, "Daemon worker starting flag (Optional)")
	flagSet.StringVar(&args.ConfigPath, "config-path", "", "Path to the configuration file (Mandatory)")
	flagSet.SetOutput(os.Stderr)

	if err := flagSet.Parse(os.Args[1:]); err != nil {
		log.Fatalf("Error: %v", err)
	}
	return args, flagSet
}

func startDaemonWorkers() {
	retriggerEventFile()
	go housekeeper()
	monitorJobs()
}

func startOneTimeWorkers() {
	var (
		data      common.Data
		ed        common.EventData
		agentData common.AgentDataW
	)

	agentDataBytes, err := io.ReadAll(os.Stdin)
	if err != nil {
		os.Exit(1)
	}

	if err = json.Unmarshal(agentDataBytes, &agentData); err != nil {
		os.Exit(1)
	}

	data.EventData = agentData.EventData
	json.Unmarshal(agentData.EventData, &ed)

	if agentData.SocketFilePath != "" {
		var err error
		if data.NetConn, err = agentutils.RecreateSocket(agentData.SocketFilePath); err != nil {
			os.Exit(1)
		}
	}

	ProcessEventData(data)
}

func main() {
	arg, _ = parseArgs()

	if err := conf.Load(arg.ConfigPath, &agent.Options); err != nil {
		os.Exit(1)
	}

	common.ConfigFilePath = arg.ConfigPath
	common.Manager.Name = common.AgentManagerProcess
	common.LockFilePath = filepath.Join(agent.Options.TmpDir, "lock_file.lock")

	if err := logger.InitLogger(
		agent.Options.JaLogFile,
		agent.Options.JaLogMessageFile,
		agent.Options.LogType,
		agent.Options.LogFileSize*1024*1024,
		agent.Options.DebugLevel,
		logger.TargetTypeAgent,
	); err != nil {
		os.Exit(1)
	}

	//catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			//output stacktrace
			logger.WriteLog("JAAGENT100003", string(debug.Stack()))
			os.Exit(1)
		}
	}()

	initValue()

	if arg.Daemon {
		startDaemonWorkers()
	} else {
		startOneTimeWorkers()
	}
}
