/*
** 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 (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"jobarranger2/src/libs/golibs/builder"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/server"
	"jobarranger2/src/libs/golibs/database"
	"jobarranger2/src/libs/golibs/logger/logger"
)

func SetEnd(innerJobID uint64, msgFlag int, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "SetEnd"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400003", *logData, funcName, innerJobID, msgFlag)

	message := processData.JobResult.Message

	if msgFlag == 0 {
		return nil
	}

	if err := logger.JaJobLog(common.JC_JOB_END, *logData); err != nil {
		return err
	}

	// for icon retry case
	// icon run success after 2 times retry, need to insert jobarg_message into after variable for retry rounds.
	if message != "" {
		err = MergeAfterVariable(processData, "JOBARG_MESSAGE", message)
		if err != nil {
			logger.JaLog("JAICONRESULTNORMAL200006", *logData, funcName, "JOBARG_MESSAGE", message, innerJobID)
		}
	}

	return SetJobStatus(innerJobID, int(common.StatusEnd), -1, 1, processData, nextdata)

}

func SetRunError(innerJobID uint64, iconStatus int, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "SetRunError"

	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400004", *logData, funcName, innerJobID, iconStatus)

	var jobExitCodeStr, jobnetID, execUsername, jobID string

	jobID = processData.RunJobData.JobID
	innerJobnetID := processData.RunJobData.InnerJobnetID
	innerJobnetMainID := processData.RunJobData.InnerJobnetMainID
	jobType := processData.RunJobData.IconType

	if err := logger.JaJobLog(common.JC_JOB_ERR_END, *logData); err != nil {
		return err
	}

	if jobType == common.JobTypeJob && iconStatus != -99 {
		err := MergeAfterVariable(processData, "ICON_STATUS", iconStatus)
		if err != nil {
			logger.JaLog("JAICONRESULTNORMAL200006", *logData, funcName, "ICON_STATUS", iconStatus, innerJobID)
			return err
		} else {
			nextdata.NextProcess.Data = common.FlowProcessData{
				Status: common.StatusType(iconStatus),
			}
		}
	}

	if jobType != common.JobTypeJobnet {
		if innerJobnetMainID != 0 {
			jobnetID = processData.RunJobnetData.JobnetID
			execUsername = processData.RunJobnetData.ExecutionUserName
		}
		if jobType == common.JobTypeJob || jobType == common.JobTypeLess {
			jobExitCodeStr = fmt.Sprintf("%v", processData.JobResult.ReturnCode)
		}
		logger.JaLog("JAICONRESULTNORMAL000001", *logData, funcName, innerJobnetID, jobnetID, jobID, execUsername, jobExitCodeStr, iconStatus)
	}

	return SetJobStatus(innerJobID, int(common.StatusRunErr), -1, 1, processData, nextdata)

}

func SetEndError(innerJobID uint64, msgFlag int, processData *common.IconExecutionProcessData, nextData *common.EventData) error {
	const funcName = "SetEndError"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400003", *logData, funcName, innerJobID, msgFlag)

	jobID := processData.RunJobData.JobID

	if msgFlag == 1 {

		if err := logger.JaJobLog(common.JC_JOB_ERR_END, *logData); err != nil {
			return err
		}

		innerJobnetMainID := processData.RunJobData.InnerJobnetMainID

		if innerJobnetMainID != 0 {
			jobnetID := processData.RunJobnetData.JobnetID
			execUsername := processData.RunJobnetData.ExecutionUserName
			logger.JaLog("JAICONRESULTNORMAL200002", *logData, funcName, innerJobID, jobnetID, jobID, execUsername)
		}
	}

	return SetJobStatus(innerJobID, int(common.StatusEndErr), -1, 1, processData, nextData)
}

func SetJobStatus(innerJobID uint64, status, start, end int, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "SetJobStatus"
	var setStart, setEnd, lockQuery, updateQuery, endCountSet string
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400004", *logData, funcName, innerJobID, status)

	now := time.Now().UnixNano()

	switch start {
	case 0:
		setStart = ", start_time = 0"
	case 1:
		setStart = fmt.Sprintf(", start_time = %d", now)
	}

	switch end {
	case 0:
		setEnd = ", end_time = 0"
	case 1:
		setEnd = fmt.Sprintf(", end_time = %d", now)
	}

	lockQuery = fmt.Sprintf("SELECT status FROM %s WHERE inner_job_id = ? FOR UPDATE", common.Ja2RunJobTable)
	nextdata.AddPQ(lockQuery, innerJobID)

	switch status {
	case int(common.StatusEnd):
		// If icon type is W (Multiple end icon), reset end_count to 0
		if processData.RunJobData.IconType == common.IconTypeW {
			endCountSet = ", end_count = 0"
		}
		updateQuery = fmt.Sprintf("UPDATE %s SET status = ?%s%s%s, retry_count = 0 WHERE inner_job_id = ?", common.Ja2RunJobTable, setStart, setEnd, endCountSet)
		nextdata.AddPQ(updateQuery, status, innerJobID)
	case int(common.StatusRunErr):
		updateQuery = fmt.Sprintf("UPDATE %s SET status = ?%s%s, retry_count = 0 WHERE inner_job_id = ?", common.Ja2RunJobTable, setStart, setEnd)
		nextdata.AddPQ(updateQuery, status, innerJobID)
	case int(common.StatusEndErr):
		updateQuery = fmt.Sprintf("UPDATE %s SET status = ?%s%s WHERE inner_job_id = ?", common.Ja2RunJobTable, setStart, setEnd)
		nextdata.AddPQ(updateQuery, status, innerJobID)
	default:
		//
	}

	switch status {
	case int(common.StatusEnd):
		nextdata.Event.Name = common.EventIconResultEnd
		// Build a SQLCondition
		sqlCond := builder.NewSQLCondition(
			fmt.Sprintf("select status, method_flag from %s where inner_job_id = %d", common.Ja2RunJobTable, innerJobID),
		).
			AddCondition(
				builder.NewCondition(common.ActionIgnore).
					Field("status", common.OpEq, common.StatusEnd).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionWait).
					Field("status", common.OpEq, common.StatusReady).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionWait).
					Field("status", common.OpEq, common.StatusReady).
					Field("method_flag", common.OpEq, common.MethodRerun).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusReady).
					Field("method_flag", common.OpEq, common.MethodSkip).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusRun).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusRun).
					Field("method_flag", common.OpEq, common.MethodRerun).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusRunErr).
					Field("method_flag", common.OpEq, common.MethodSkip).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusEndErr).
					Field("method_flag", common.OpEq, common.MethodSkip).
					Build(),
			).
			AddCondition(
				builder.NewCondition(common.ActionExec).
					Field("status", common.OpEq, common.StatusAbort).
					Field("method_flag", common.OpEq, common.MethodAbort).
					Build(),
			).
			DefaultAction(common.ActionError).
			Build()
		nextdata.SQLConditions = []common.SQLCondition{sqlCond}
	case int(common.StatusRunErr), int(common.StatusAbort):
		nextdata.Event.Name = common.EventIconResultRunErr

		// to ignore the abort event happened at ready status if icon turned other status in dbsyncer
		if processData.RunJobData.Status == common.StatusReady && processData.RunJobData.MethodFlag == common.MethodAbort {
			sqlCond := builder.NewSQLCondition(
				fmt.Sprintf("select status from %s where inner_job_id = %d", common.Ja2RunJobTable, innerJobID),
			).
				AddCondition(
					builder.NewCondition(common.ActionExec).
						Field("status", common.OpEq, common.StatusReady).
						Build(),
				).
				DefaultAction(common.ActionIgnore).
				Build()
			nextdata.SQLConditions = []common.SQLCondition{sqlCond}
		} else {
			sqlCondBuilder := builder.NewSQLCondition(
				fmt.Sprintf("select status, method_flag from %s where inner_job_id = %d", common.Ja2RunJobTable, innerJobID),
			).AddCondition(
				builder.NewCondition(common.ActionIgnore).
					Field("status", common.OpEq, common.StatusEnd).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).AddCondition(
				builder.NewCondition(common.ActionIgnore).
					Field("status", common.OpEq, common.StatusRunErr).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).DefaultAction(common.ActionExec)

			//add condition to fix timing issue of changing runerr before run at dbsyncer
			if processData.JobResult.Result != common.JA_JOBRESULT_PRE_EXEC_FAIL {
				sqlCondBuilder.AddCondition(
					builder.NewCondition(common.ActionWait).
						Field("status", common.OpEq, common.StatusReady).
						Field("method_flag", common.OpEq, common.MethodNormal).
						Build(),
				).AddCondition(
					builder.NewCondition(common.ActionWait).
						Field("status", common.OpEq, common.StatusReady).
						Field("method_flag", common.OpEq, common.MethodRerun).
						Build(),
				)
			}
			nextdata.SQLConditions = []common.SQLCondition{sqlCondBuilder.Build()}
		}
	case int(common.StatusEndErr):
		nextdata.Event.Name = common.EventIconResultEndErr
		sqlCond := builder.NewSQLCondition(
			fmt.Sprintf("select status, method_flag from %s where inner_job_id = %d", common.Ja2RunJobTable, innerJobID),
		).
			AddCondition(
				builder.NewCondition(common.ActionIgnore).
					Field("status", common.OpEq, common.StatusEnd).
					Field("method_flag", common.OpEq, common.MethodNormal).
					Build(),
			).
			DefaultAction(common.ActionExec).
			Build()
		nextdata.SQLConditions = []common.SQLCondition{sqlCond}
	default:
		//
	}

	if existingFlowData, ok := nextdata.NextProcess.Data.(common.FlowProcessData); ok {
		existingFlowData.Status = common.StatusType(status)
		nextdata.NextProcess.Data = existingFlowData
	} else {
		// fallback if not previously set
		nextdata.NextProcess.Data = common.FlowProcessData{
			Status: common.StatusType(status),
		}
	}

	return nil
}

func SetValueAfter(innerJobID uint64, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "SetValueAfter"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400002", *logData, funcName, innerJobID)

	innerJobnetID := processData.RunJobVariableData.InnerJobnetID
	seqNo := processData.RunJobVariableData.SeqNo
	dbType := server.Options.DBType

	var after map[string]any

	jsonBytes, err := json.Marshal(processData.RunJobVariableData.AfterVariable)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200003", *logData, funcName, innerJobID, processData.RunJobVariableData.AfterVariable, err.Error())
		return err
	}

	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
	decoder.UseNumber()
	if err := decoder.Decode(&after); err != nil {
		logger.JaLog("JAICONRESULTNORMAL200005", *logData, funcName, innerJobID, &after, err.Error())
		return err
	}

	jobArgMsg, hasJobArg := after["JOBARG_MESSAGE"]

	isEmpty := !hasJobArg || jobArgMsg == nil || jobArgMsg == ""

	// Marshal the JSON back
	afterJSON, err := json.Marshal(after)
	if err != nil {
		return fmt.Errorf("marshal after_variable: %w", err)
	}

	var updateQuery string
	args := []any{
		string(afterJSON), innerJobID, innerJobnetID, seqNo,
	}
	if isEmpty {
		// Don’t touch JOBARG_MESSAGE, just merge others
		switch dbType {
		case "postgres":
			updateQuery = fmt.Sprintf(
				"UPDATE %s "+
					"SET after_variable = after_variable || (?::jsonb - 'JOBARG_MESSAGE') "+
					"WHERE inner_job_id = ? AND inner_jobnet_id = ? AND seq_no = ?", common.Ja2RunJobVariableTable,
			)
		case "mysql":
			updateQuery = fmt.Sprintf(
				"UPDATE %s "+
					"SET after_variable = JSON_MERGE_PATCH(after_variable, JSON_REMOVE(CAST(? AS JSON), '$.JOBARG_MESSAGE')) "+
					"WHERE inner_job_id = ? AND inner_jobnet_id = ? AND seq_no = ?", common.Ja2RunJobVariableTable,
			)
		case "maria":
			updateQuery = fmt.Sprintf(
				"UPDATE %s "+
					"SET after_variable = JSON_MERGE_PATCH(after_variable, JSON_REMOVE(?, '$.JOBARG_MESSAGE')) "+
					"WHERE inner_job_id = ? AND inner_jobnet_id = ? AND seq_no = ?", common.Ja2RunJobVariableTable,
			)
		default:
			return fmt.Errorf("unsupported dbType: %s", dbType)
		}
	} else {
		// Overwrite fully if JOBARG_MESSAGE is present
		updateQuery = fmt.Sprintf(
			"UPDATE %s "+
				"SET after_variable = ? "+
				"WHERE inner_job_id = ? AND inner_jobnet_id = ? AND seq_no = ?", common.Ja2RunJobVariableTable,
		)
	}

	nextdata.AddPQ(updateQuery, args...)
	return nil
}

func FillAfterVariable(innerJobID uint64, processData *common.IconExecutionProcessData, withJobOutputs bool) error {
	const funcName = "FillAfterVariable"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}
	logger.JaLog("JAICONRESULTNORMAL400002", *logData, funcName, innerJobID)

	var after map[string]any

	jsonBytes, err := json.Marshal(processData.RunJobVariableData.BeforeVariable)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200003", *logData, funcName, innerJobID, processData.RunJobVariableData.BeforeVariable, err.Error())
		return err
	}

	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
	decoder.UseNumber()
	if err := decoder.Decode(&after); err != nil {
		logger.JaLog("JAICONRESULTNORMAL200005", *logData, funcName, innerJobID, &after, err.Error())
		return err
	}

	// only add Job outputs if caller requests
	if withJobOutputs {
		jobType := processData.RunJobData.IconType
		jobResult := processData.JobResult.Result
		if jobType == common.JobTypeJob || jobType == common.JobTypeFwait || jobType == common.JobTypeReboot || jobType == common.JobTypeLess {
			if processData.JobResult.Message != "" {
				after["JOBARG_MESSAGE"] = processData.JobResult.Message
			}
			if jobResult == common.JA_JOBRESULT_SUCCEED || jobResult == common.JA_JOBRESULT_FAIL {
				after["STD_OUT"] = common.TruncateTo64KB(processData.JobResult.StdOut)
				after["STD_ERR"] = common.TruncateTo64KB(processData.JobResult.StdErr)
				after["JOB_EXIT_CD"] = fmt.Sprintf("%v", processData.JobResult.ReturnCode)
				after["SIGNAL"] = strconv.Itoa(processData.JobResult.Signal)
			} else {
				if jobType == common.JobTypeLess {
					after["STD_OUT"] = common.TruncateTo64KB(processData.JobResult.StdOut)
					after["STD_ERR"] = common.TruncateTo64KB(processData.JobResult.StdErr)
					after["JOB_EXIT_CD"] = ""
					after["SIGNAL"] = strconv.Itoa(processData.JobResult.Signal)
				} else {
					delete(after, "STD_OUT")
					delete(after, "STD_ERR")
					delete(after, "JOB_EXIT_CD")
					delete(after, "SIGNAL")
				}
			}
		}
	}

	updated, err := json.Marshal(after)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200003", *logData, funcName, innerJobID, after, err.Error())
		return err
	}

	processData.RunJobVariableData.AfterVariable = updated

	return nil
}

func CopyValue(innerJobID uint64, valueSrc string, beforeVar json.RawMessage, processData *common.IconExecutionProcessData) (string, error) {
	const funcName = "CopyValue"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return "", err
	}

	logger.JaLog("JAICONRESULTNORMAL400002", *logData, funcName, innerJobID)

	if valueSrc == "" {
		return "", errors.New("empty source value")
	}

	// Case: "$KEY" → lookup in beforeVariable
	if strings.HasPrefix(valueSrc, "$") && !strings.HasPrefix(valueSrc, "$$") {
		key := valueSrc[1:]
		return GetValueFromBeforeVariable(innerJobID, key, beforeVar, processData)
	}

	// Case: "$$literal" → return "$literal"
	if strings.HasPrefix(valueSrc, "$$") {
		return valueSrc[1:], nil
	}

	// Case: normal value → return as-is
	return valueSrc, nil
}

func GetValueFromBeforeVariable(innerJobID uint64, key string, beforeVar json.RawMessage, processData *common.IconExecutionProcessData) (string, error) {
	// const funcName = "GetValueFromBeforeVariable"
	var beforeMap map[string]interface{}
	// logData := &logger.Logging{}
	// logData, err := getLogData(processData)
	// if err != nil {
	// 	logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
	// 	return "", err
	// }

	// logger.JaLog("JAICONRESULTNORMAL400002", *logData, funcName, innerJobID)
	if len(beforeVar) == 0 || string(beforeVar) == "null" {
		return "", fmt.Errorf("before variable is empty or null")
	}

	if err := json.Unmarshal(beforeVar, &beforeMap); err != nil {
		return "", fmt.Errorf("failed to unmarshal BeforeVariable: %v", err)
	}

	if val, ok := beforeMap[key]; ok {
		return fmt.Sprintf("%v", val), nil
	}
	return "", fmt.Errorf("key '%s' not found in BeforeVariable", key)
}

func GetValueFromAfterVariable(innerJobID uint64, key string, afterVar json.RawMessage, processData *common.IconExecutionProcessData) (string, error) {
	const funcName = "GetValueFromAfterVariable"
	var afterMap map[string]interface{}
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return "", err
	}

	logger.JaLog("JAICONRESULTNORMAL400002", *logData, funcName, innerJobID)

	if len(afterVar) == 0 || string(afterVar) == "null" {
		return "", fmt.Errorf("after variable is empty or null")
	}

	if err := json.Unmarshal(afterVar, &afterMap); err != nil {
		return "", fmt.Errorf("failed to unmarshal AfterVariable: %v", err)
	}

	if val, ok := afterMap[key]; ok {
		return fmt.Sprintf("%v", val), nil
	}
	return "", fmt.Errorf("key '%s' not found in AfterVariable", key)
}

// JaNumberMatch compares a numeric string against one or more numeric patterns (e.g. "-10", "5-10", etc.).
func JaNumberMatch(input string, pattern string, processData *common.IconExecutionProcessData) int {
	const funcName = "JaNumberMatch"
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return -1
	}

	input = strings.TrimSpace(input)
	pattern = strings.TrimSpace(pattern)
	if input == "" || pattern == "" {
		return 0
	}

	logger.JaLog("JAICONRESULTNORMAL400008", *logData, funcName, input, pattern)

	// Step 1: Parse input to float
	value, err := strconv.ParseFloat(input, 64)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL300001", *logData, funcName, input)
		return -1
	}

	// Step 2: Split pattern by comma for multiple patterns
	tokens := strings.Split(pattern, ",")
	for _, token := range tokens {
		token = strings.TrimSpace(token)
		if token == "" {
			continue
		}

		res := JaNumberCompare(value, token, processData)
		if res == 1 {
			return 1 // match found
		}
	}

	return 0 // no match
}

// JaNumberCompare checks if num fits the given range string like "-10", "5-10", etc.
func JaNumberCompare(num float64, rangeStr string, processData *common.IconExecutionProcessData) int {
	const funcName = "JaNumberCompare"
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return -1
	}

	rangeStr = strings.TrimSpace(rangeStr)
	if rangeStr == "" {
		return -1
	}

	logger.JaLog("JAICONRESULTNORMAL400009", *logData, funcName, num, rangeStr)

	// Only treat as range if it contains "-" but does NOT start with "-" (negative numbers)
	if strings.Count(rangeStr, "-") == 1 && !strings.HasPrefix(rangeStr, "-") {
		parts := strings.SplitN(rangeStr, "-", 2)
		start, err1 := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
		end, err2 := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
		if err1 != nil || err2 != nil {
			logger.JaLog("JAICONRESULTNORMAL300002", *logData, funcName, rangeStr)
			return -1
		}

		if start > end {
			logger.JaLog("JAICONRESULTNORMAL300003", *logData, funcName, start, end)
			return -1
		}

		if num >= start && num <= end {
			return 1
		}
		return 0
	}

	// Single numeric value (supports negative numbers now)
	singleVal, err := strconv.ParseFloat(rangeStr, 64)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL300002", *logData, funcName, rangeStr)
		return -1
	}

	if num == singleVal {
		return 1
	}

	return 0
}

func ClearEventData(e *common.EventData, p *common.IconExecutionProcessData) error {
	const funcName = "ClearEventData"
	logData := &logger.Logging{}
	logData, err := getLogData(p)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400012", *logData, funcName, e)

	if e != nil {
		// clear fields used only for forwarding
		e.Event = common.Event{}
		e.NextProcess = common.NextProcess{}
		e.Transfer = common.Transfer{}
		e.SQLConditions = nil
		e.ParameterizedQueries = nil
		e.TCPMessage = nil
		e.DBSyncResult = nil
	}

	return nil
}

func UpdateSession(rtn int, operationFlag int, sessionID string, jobnetMainID uint64, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "UpdateSession"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200013", logger.Logging{}, funcName, err.Error())
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400010", *logData, funcName, rtn, operationFlag, sessionID, jobnetMainID)

	if sessionID == "" || jobnetMainID == 0 {
		return fmt.Errorf("invalid sessionID or jobnetMainID")
	}

	if operationFlag == common.JA_SES_OPERATION_FLAG_ONETIME ||
		(operationFlag == common.JA_SES_OPERATION_FLAG_CONNECT && rtn == common.JA_JOBRESULT_FAIL) ||
		(operationFlag == common.JA_SES_OPERATION_FLAG_CLOSE && rtn == common.JA_JOBRESULT_SUCCEED) {

		deleteQuery := fmt.Sprintf(
			"DELETE FROM %s WHERE session_id = ? AND inner_jobnet_main_id = ?",
			common.Ja2SessionTable)
		nextdata.AddPQ(deleteQuery, sessionID, jobnetMainID)

	} else {
		// Update session status to END
		selectQuery := fmt.Sprintf("SELECT inner_job_id FROM %s WHERE session_id = ? AND inner_jobnet_main_id = ? FOR UPDATE", common.Ja2SessionTable)
		nextdata.AddPQ(selectQuery, sessionID, jobnetMainID)
		updateQuery := fmt.Sprintf("UPDATE %s SET status = ? WHERE session_id = ? AND inner_jobnet_main_id = ?", common.Ja2SessionTable)
		nextdata.AddPQ(updateQuery, common.JA_SES_STATUS_END, sessionID, jobnetMainID)
	}

	return nil
}

func getAgentData(conn database.DBConnection, innerJobID uint64) (
	common.IconType,
	common.AgentJobStatus,
	map[string]interface{}, // ← return full JSON data
	uint64,
	string,
	error,
) {
	const funcName = "getAgentData"

	query := fmt.Sprintf(`
        SELECT 
            r.job_type, 
            r.status, 
            r.data, 
            r.inner_jobnet_id,
            n.jobnet_id
        FROM %s r
        JOIN %s n 
          ON r.inner_jobnet_id = n.inner_jobnet_id
        WHERE r.inner_job_id = %d
    `, common.Ja2RunJobTable, common.Ja2RunJobnetTable, innerJobID)

	dbResult, err := conn.Select(query)
	if err != nil {
		return 0, 0, nil, 0, "", err
	}
	defer dbResult.Free()

	row, err := dbResult.Fetch()
	if err != nil {
		return 0, 0, nil, 0, "", err
	}

	iconTypeStr := row["job_type"]
	statusStr := row["status"]
	dataStr := row["data"]
	innerJobnetIDStr := row["inner_jobnet_id"]
	jobnetID := row["jobnet_id"]

	if iconTypeStr == "" || statusStr == "" || dataStr == "" ||
		innerJobnetIDStr == "" || jobnetID == "" {
		return 0, 0, nil, 0, "", fmt.Errorf("[%s]: one or more required fields are empty", funcName)
	}

	// Unmarshal full JSON field into map
	var jsonData map[string]interface{}
	if err := json.Unmarshal([]byte(dataStr), &jsonData); err != nil {
		return 0, 0, nil, 0, "", fmt.Errorf("[%s]: invalid JSON in data: %v", funcName, err)
	}

	// Convert numerics
	iconTypeInt, err := strconv.Atoi(iconTypeStr)
	if err != nil {
		return 0, 0, nil, 0, "", fmt.Errorf("[%s]: invalid icon type: %v", funcName, err)
	}

	statusInt, err := strconv.Atoi(statusStr)
	if err != nil {
		return 0, 0, nil, 0, "", fmt.Errorf("[%s]: invalid status: %v", funcName, err)
	}

	innerJobnetID, err := strconv.ParseUint(innerJobnetIDStr, 10, 64)
	if err != nil {
		return 0, 0, nil, 0, "", fmt.Errorf("[%s]: invalid inner_jobnet_id: %v", funcName, err)
	}

	return common.IconType(iconTypeInt),
		common.AgentJobStatus(statusInt),
		jsonData, // ← return the entire JSON object
		innerJobnetID,
		jobnetID,
		nil
}

func getVariableData(conn database.DBConnection, innerJobID uint64) (*common.RunJobVariableTable, error) {
	const funcName = "getVariableData"

	query := fmt.Sprintf(`
		SELECT seq_no, inner_job_id, inner_jobnet_id, before_variable, after_variable
		FROM %s
		WHERE inner_job_id = %d
		LIMIT 1`, common.Ja2RunJobVariableTable, innerJobID)

	dbResult, err := conn.Select(query)
	if err != nil {
		return nil, err
	}
	defer dbResult.Free()

	row, err := dbResult.Fetch()
	if err != nil {
		return nil, err
	}

	var result common.RunJobVariableTable

	// Parse seq_no
	if row["seq_no"] != "" {
		if v, err := strconv.ParseUint(row["seq_no"], 10, 64); err == nil {
			result.SeqNo = int(v)
		} else {
			return nil, fmt.Errorf("[%s]: failed to parse seq_no '%s': %v", funcName, row["seq_no"], err)
		}
	}

	// Parse inner_job_id
	if row["inner_job_id"] != "" {
		if v, err := strconv.ParseUint(row["inner_job_id"], 10, 64); err == nil {
			result.InnerJobID = v
		} else {
			return nil, fmt.Errorf("[%s]: failed to parse inner_job_id '%s': %v", funcName, row["inner_job_id"], err)
		}
	}

	// Parse inner_jobnet_id
	if row["inner_jobnet_id"] != "" {
		if v, err := strconv.ParseUint(row["inner_jobnet_id"], 10, 64); err == nil {
			result.InnerJobnetID = v
		} else {
			return nil, fmt.Errorf("[%s]: failed to parse inner_jobnet_id '%s': %v", funcName, row["inner_jobnet_id"], err)
		}
	}

	// Parse JSONB fields
	if row["before_variable"] != "" {
		result.BeforeVariable = json.RawMessage(row["before_variable"])
	}
	if row["after_variable"] != "" {
		result.AfterVariable = json.RawMessage(row["after_variable"])
	}

	return &result, nil
}

func getLogData(processData *common.IconExecutionProcessData) (*logger.Logging, error) {

	jobIDStr := processData.JobResult.JobRunRequestData.JobID
	if jobIDStr == "" {
		return nil, fmt.Errorf("job ID is empty")
	}

	innerJobID, err := strconv.ParseUint(jobIDStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid job ID '%s': %w", jobIDStr, err)
	}

	logData := &logger.Logging{
		InnerJobID:        innerJobID,
		InnerJobnetID:     processData.RunJobnetData.InnerJobnetID,
		InnerJobnetMainID: processData.RunJobnetData.InnerJobnetMainID,
		JobnetStatus:      processData.RunJobnetData.Status,
		JobnetID:          processData.RunJobnetData.JobnetID,
		UpdateDate:        processData.RunJobnetData.UpdateDate,
		RunType:           processData.RunJobnetData.RunType,
		PublicFlag:        processData.RunJobnetData.PublicFlag,
		JobnetName:        processData.RunJobnetData.JobnetName,
		UserName:          processData.RunJobnetData.ExecutionUserName,

		MethodFlag: processData.RunJobData.MethodFlag,
		JobID:      processData.RunJobData.JobID,
		JobName:    processData.RunJobData.JobName,
		JobType:    processData.RunJobData.IconType,
		JobStatus:  common.StatusRun,
	}

	if processData.RunJobData.IconType == common.IconTypeJob {
		logData.JobExitCDValue = "JOB_EXIT_CD"
		logData.JobExitCD = toInt(processData.JobResult.ReturnCode)
		logData.StdOutValue = "STD_OUT"
		logData.StdOut = common.TruncateTo64KB(processData.JobResult.StdOut)
		logData.StdErrValue = "STD_ERR"
		logData.StdErr = common.TruncateTo64KB(processData.JobResult.StdErr)
	}

	if processData.RunJobData.IconType == common.IconTypeLess {
		iconLessData, err := ExtractIconData[common.IconLessData](processData.RunJobData.Data)
		if err != nil {
			return nil, fmt.Errorf("invalid runjobdata '%v': %s", processData.RunJobData.Data, err.Error())
		}
		logData.SessionFlag = iconLessData.SessionFlag
		logData.JobExitCDValue = "JOB_EXIT_CD"
		logData.JobExitCD = toInt(processData.JobResult.ReturnCode)
		logData.StdOutValue = "STD_OUT"
		logData.StdOut = common.TruncateTo64KB(processData.JobResult.StdOut)
		logData.StdErrValue = "STD_ERR"
		logData.StdErr = common.TruncateTo64KB(processData.JobResult.StdErr)
	}

	return logData, nil
}

func MergeAfterVariable(processData *common.IconExecutionProcessData, key string, value any) error {
	const funcName = "MergeAfterVariable"
	logData, _ := getLogData(processData)
	logger.JaLog("JAICONRESULTNORMAL400011", *logData, funcName, key, value)

	var m map[string]any

	orig := processData.RunJobVariableData.AfterVariable
	if len(orig) == 0 || string(orig) == "null" {
		m = make(map[string]any)
	} else {
		decoder := json.NewDecoder(bytes.NewReader(orig))
		decoder.UseNumber() // <-- This is the key!
		err := decoder.Decode(&m)
		if err != nil {
			return err
		}
	}

	m[key] = value

	updated, err := json.Marshal(m)
	if err != nil {
		return err
	}

	processData.RunJobVariableData.AfterVariable = updated
	return nil
}

func handleIconError(innerJobID uint64, status int, message string, processData *common.IconExecutionProcessData, nextdata *common.EventData) error {
	const funcName = "handleIconError"
	logData := &logger.Logging{}
	logData, err := getLogData(processData)
	if err != nil {
		return err
	}

	logger.JaLog("JAICONRESULTNORMAL400013", *logData, funcName, innerJobID, status, message)

	err = MergeAfterVariable(processData, "JOBARG_MESSAGE", message)
	if err != nil {
		logger.JaLog("JAICONRESULTNORMAL200006", *logData, funcName, "JOBARG_MESSAGE", message, innerJobID)
	}

	return SetRunError(innerJobID, status, processData, nextdata)

}

func ExtractIconData[T any](data interface{}) (*T, error) {
	rawBytes, err := json.Marshal(data)
	if err != nil {
		return nil, fmt.Errorf("marshal failed: %w", err)
	}

	var result T
	if err := json.Unmarshal(rawBytes, &result); err != nil {
		return nil, fmt.Errorf("unmarshal failed: %w", err)
	}
	return &result, nil
}

func toInt(v any) int {
	switch val := v.(type) {
	case float64:
		return int(val)
	case string:
		i, err := strconv.Atoi(val)
		if err != nil {
			return 0
		}
		return i
	default:
		return 0
	}
}
