/*
** 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 logger

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/eventcore"
)

// logger Struct
type Logger struct {
	mu             sync.Mutex
	logFile        *os.File
	logFilePath    string
	currentSize    int64
	maxLogFileSize int64
	msgFilePath    string
	logMessages    map[string]Message // message_id as keys
	logType        LogType
	logLevel       LogLevel
	targetType     int
	systemLog      SystemLogger
}

type SystemLogger interface {
	WriteLog(logLevel LogLevel, message string) error
	Close() error
}

type MutexLocker interface {
	LockFile(f *os.File) error
	UnlockFile(f *os.File) error
}

// Target Type (server, agent, module)
const (
	TargetTypeServer = 0
	TargetTypeAgent  = 1
	TargetTypeModule = 2
)

// Log level
type LogLevel int

const (
	LogLevelUnsupported LogLevel = -1
	LogLevelInfo        LogLevel = 0
	LogLevelCrit        LogLevel = 1
	LogLevelErr         LogLevel = 2
	LogLevelWarn        LogLevel = 3
	LogLevelDebug       LogLevel = 4
)

// Log type
type LogType int

const (
	LogTypeUnsupported LogType = -1
	LogTypeFile        LogType = 0
	LogTypeSystem      LogType = 1
	LogTypeConsole     LogType = 2
)

// Support log type params
const (
	LogTypeFileParam    = "file"
	LogTypeSystemParam  = "system"
	LogTypeConsoleParam = "console"
)

func (l LogLevel) String() string {
	switch l {
	case LogLevelInfo:
		return "INFO"
	case LogLevelCrit:
		return "CRITICAL"
	case LogLevelErr:
		return "ERROR"
	case LogLevelWarn:
		return "WARNING"
	case LogLevelDebug:
		return "DEBUG"
	default:
		return "UNKNOWN"
	}
}

type Message struct {
	ID         string
	LogLevel   LogLevel // 0:information, 1:critical, 2:error, 3:warning
	NoticeFlag int      // 0:No error notification, 1:Error notification
	Body       string   // Message body with format specifiers
}

const NotFoundMsgID = "JALOGGER200001"
const CannotRenameMsgID = "JALOGGER200004"
const TruncateMsgID = "JALOGGER200005"

type Logging struct {
	// ja_2_run_jobnet_table
	InnerJobnetMainID uint64            `json:"inner_jobnet_main_id"`
	InnerJobnetID     uint64            `json:"inner_jobnet_id"`
	UpdateDate        uint64            `json:"update_date"`
	JobnetStatus      common.StatusType `json:"jobnet_status"`
	RunType           int               `json:"run_type"`
	PublicFlag        int               `json:"public_flag"`
	JobnetID          string            `json:"jobnet_id"`
	JobnetName        string            `json:"jobnet_name"`
	UserName          string            `json:"user_name"`

	// ja_2_run_job_table
	InnerJobID uint64              `json:"inner_job_id,omitempty"`
	JobType    common.IconType     `json:"job_type,omitempty"`
	MethodFlag common.JobRunMethod `json:"method_flag,omitempty"`
	JobStatus  common.StatusType   `json:"job_status,omitempty"`
	JobID      string              `json:"job_id,omitempty"`
	JobName    string              `json:"job_name,omitempty"`

	//ja_2_run_job_table.Data( if agentless icon)
	SessionFlag int `json:"session_flag,omitempty"` // only for agentless icon

	// ja_2_run_variable_table (after_variable)
	// only for job_icon and agentless_icon
	JobExitCDValue string `json:"job_exit_cd_value,omitempty"`
	JobExitCD      int    `json:"job_exit_cd,omitempty"`
	StdOutValue    string `json:"std_out_value,omitempty"`
	StdOut         string `json:"std_out,omitempty"`
	StdErrValue    string `json:"std_err_value,omitempty"`
	StdErr         string `json:"std_err,omitempty"`
}

func (log *Logging) JaJobLog(messageId string) error {
	return JaJobLog(messageId, *log)
}

func (log *Logging) JaLog(messageId string, varags ...any) (string, error) {
	return JaLog(messageId, *log, varags...)
}

var logger Logger

// targetType: 0 - server, 1 - agent
func InitLogger(logFilePath string, messageFilePath string, logTypeStr string, maxLogFileSize int, logLevelInt int, targetType int) error {
	// Check and get log type
	logType, err := GetLogTypeFromStr(logTypeStr)
	if err != nil {
		return err
	}

	if targetType < 0 || targetType > 2 {
		return fmt.Errorf("invalid target type: %d. supported: 0 - server, 1 - agent, 2 - module", targetType)
	}

	// load message file
	var logMessages map[string]Message
	if targetType != 2 {
		logMessages, err = LoadLogMessageFile(messageFilePath, targetType)
		if err != nil {
			return fmt.Errorf("failed to load log message file '%s': %v", messageFilePath, err)
		}
	}

	// Initialize based on log type
	switch logType {
	case LogTypeFile:

		logger = Logger{
			logFilePath:    logFilePath,
			maxLogFileSize: int64(maxLogFileSize),
			msgFilePath:    messageFilePath,
			logMessages:    logMessages,
			logType:        logType,
			logLevel:       LogLevel(logLevelInt),
			targetType:     targetType,
		}

		logger.logFile, err = openLogFile()
		if err != nil {
			return fmt.Errorf("failed to open logFile '%s': %w", logFilePath, err)
		}

	case LogTypeSystem:
		systemLog, err := NewSystemLogger()
		if err != nil {
			return fmt.Errorf("failed to initialize system logger: %w", err)
		}
		logger = Logger{
			msgFilePath: messageFilePath,
			logMessages: logMessages,
			logType:     logType,
			systemLog:   systemLog,
			logLevel:    LogLevel(logLevelInt),
			targetType:  targetType,
		}
	case LogTypeConsole:
		logger = Logger{
			msgFilePath: messageFilePath,
			logMessages: logMessages,
			logType:     logType,
			logLevel:    LogLevel(logLevelInt),
			targetType:  targetType,
		}
	}

	return nil
}

func GetLogTypeFromStr(logTypeStr string) (LogType, error) {
	var logType LogType

	switch logTypeStr {
	case LogTypeFileParam:
		logType = LogTypeFile
	case LogTypeSystemParam:
		logType = LogTypeSystem
	case LogTypeConsoleParam:
		logType = LogTypeConsole
	default:
		return LogTypeUnsupported, fmt.Errorf("unsupported log type: %s", logTypeStr)
	}

	return logType, nil

}

func GetLogLevelFromStr(logLevelStr string) (LogLevel, error) {
	var logLevel LogLevel

	switch logLevelStr {
	case LogLevelInfo.String():
		logLevel = LogLevelInfo
	case LogLevelErr.String():
		logLevel = LogLevelErr
	case LogLevelWarn.String():
		logLevel = LogLevelWarn
	case LogLevelCrit.String():
		logLevel = LogLevelCrit
	case LogLevelDebug.String():
		logLevel = LogLevelDebug
	default:
		return LogLevelUnsupported, fmt.Errorf("unsupported log type: %s", logLevelStr)
	}

	return logLevel, nil

}

func LoadLogMessageFile(logMsgFilePath string, targetType int) (map[string]Message, error) {
	logMsgs := map[string]Message{}
	logMsgFile, err := os.OpenFile(logMsgFilePath, os.O_RDONLY, 0644)
	if err != nil {
		return logMsgs, fmt.Errorf("failed to open message file '%s': %w", logMsgFilePath, err)
	}

	defer logMsgFile.Close()

	// start parsing log messages
	scanner := bufio.NewScanner(logMsgFile)

	lineNo := 0
	// Scan all lines
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		// Skip empty or commented lines
		if line == "" || strings.HasPrefix(line, "#") {
			lineNo++
			continue
		}

		lineNo++

		// Check the correctness of the line
		var maxCols int
		if targetType == TargetTypeAgent {
			maxCols = 3
		} else if targetType == TargetTypeServer {
			maxCols = 4
		} else {
			return logMsgs, fmt.Errorf("invalid target type: %d", targetType)
		}

		parts := strings.SplitN(line, ",", maxCols)

		if len(parts) != maxCols {
			return logMsgs, fmt.Errorf("malformed line in the message file '%s' at line no. %d: %s", logMsgFilePath, lineNo, line)
		}

		currentID := strings.TrimSpace(parts[0])
		// parse log level
		logLevelInt, err := strconv.Atoi(strings.TrimSpace(parts[1]))
		logLevel := LogLevel(logLevelInt)
		if err != nil || logLevel.String() == "UNKNOWN" {
			return logMsgs, fmt.Errorf("invalid log level in the message file '%s' at line no. %d: %s", logMsgFilePath, lineNo, line)
		}

		// parse notice flag
		var noticeFlag int
		if targetType == TargetTypeServer {
			noticeFlag, err = strconv.Atoi(strings.TrimSpace(parts[2]))
			if err != nil || noticeFlag < 0 || noticeFlag > 1 {
				return logMsgs, fmt.Errorf("invalid notice flag in the message file '%s' at line no. %d: %s", logMsgFilePath, lineNo, line)
			}
		}

		logMsgs[currentID] = Message{
			ID:         currentID,
			LogLevel:   logLevel,
			NoticeFlag: noticeFlag,
			Body:       strings.TrimSpace(parts[maxCols-1]),
		}

	}

	return logMsgs, nil

}

// Find message by id from the log messages
func FindMessage(messageID string) (Message, error) {
	msg, ok := logger.logMessages[messageID]
	if !ok {
		return Message{}, fmt.Errorf("message id '%s' is not found in the message file '%s'", messageID, logger.msgFilePath)
	}

	return msg, nil

}

// Write log with message ID
func WriteLog(messageID string, varargs ...any) (*Message, error) {
	// Write log to the file
	msg, err := FindMessage(messageID)
	if err != nil {
		WriteLogHelper(LogLevelErr, "[%s %s] failed to find message: %v", NotFoundMsgID, common.Manager.Name, err)
		return nil, err
	}

	if ShouldIgnoreLog(msg.LogLevel) {
		// msg is ignored due to log level
		return nil, nil
	}

	// prefix message id
	messageFmt := fmt.Sprintf("[%s %s] %s", messageID, common.Manager.Name, msg.Body) // [MessageID ManagerName] MessageBody

	err = WriteLogHelper(msg.LogLevel, messageFmt, varargs...)
	if err != nil {
		WriteLog("JALOGGER200002", err)
		return nil, err
	}

	return &msg, nil
}

// Write log without message ID
func WriteLogHelper(logLevel LogLevel, message string, varargs ...any) error {
	if ShouldIgnoreLog(logLevel) {
		// msg is ignored due to log level
		return nil
	}

	message = fmt.Sprintf(message, varargs...)

	// In-process mutex lock
	logger.mu.Lock()
	defer logger.mu.Unlock()

	// Prepare the formatting of the logs; date, pid, etc
	now := time.Now()
	timestamp := fmt.Sprintf("%.4d%.2d%.2d:%.2d%.2d%.2d.%03d",
		now.Year(),
		int(now.Month()),
		now.Day(),
		now.Hour(),
		now.Minute(),
		now.Second(),
		now.Nanosecond()/1000000,
	)

	pid := os.Getpid()

	message = fmt.Sprintf("%6d:%s [%s] %s\n", pid, timestamp, logLevel.String(), message)

	switch logger.logType {
	case LogTypeFile:
		if logger.logFilePath == "" {
			return fmt.Errorf("file logger is not initialized")
		}

		// Get locker
		locker, err := NewMutexLocker()
		if err != nil {
			return fmt.Errorf("failed to create locker: %w", err)
		}

		// Lock the log file
		err = locker.LockFile(logger.logFile)
		if err != nil {
			return fmt.Errorf("failed to lock the file '%s': %w", logger.logFilePath, err)
		}
		defer locker.UnlockFile(logger.logFile)

		// Seek to end before writing
		_, err = logger.logFile.Seek(0, io.SeekEnd)
		if err != nil {
			return fmt.Errorf("failed to seek to end of log file '%s': %w", logger.logFilePath, err)
		}

		if err := rotateLog(); err != nil {
			return fmt.Errorf("failed to rotate log: %w", err)
		}

		_, err = fmt.Fprint(logger.logFile, message)
		if err != nil {
			return fmt.Errorf("failed to write message to log file '%s': %w", logger.logFilePath, err)
		}

		return nil

	case LogTypeSystem:
		if logger.systemLog == nil {
			return fmt.Errorf("system logger is not initialized")
		}
		return logger.systemLog.WriteLog(logLevel, message)

	case LogTypeConsole:
		_, err := fmt.Fprint(os.Stdout, message)
		return err

	default:
		return fmt.Errorf("unsupported log type")
	}
}

func openLogFile() (*os.File, error) {
	if logger.logFilePath == "" {
		return nil, fmt.Errorf("logger is not initialized")
	}

	flags := os.O_CREATE | os.O_APPEND
	if runtime.GOOS == "windows" {
		flags |= os.O_RDWR // Required for Windows locking
	} else {
		flags |= os.O_WRONLY // Works on Unix
	}

	// Create file if not exists. If exists, open with Write option and append
	f, err := os.OpenFile(logger.logFilePath, flags, 0664)
	if err != nil {
		return nil, err
	}

	// Change permission
	if err = os.Chmod(logger.logFilePath, 0664); err != nil {
		return nil, err
	}

	return f, nil
}

func rotateLog() error {
	// Check for max size to rotate
	if logger.maxLogFileSize != 0 {
		// Get the log file info
		info, err := os.Stat(logger.logFilePath)
		if err != nil {
			return fmt.Errorf("failed to get file info: %w", err)
		}

		// Stat the currently opened file descriptor
		fdInfo, err := logger.logFile.Stat()
		if err != nil {
			return fmt.Errorf("stat current log fd failed: %w", err)
		}

		// Reopen the file if logfile is rotated (no longer the same file)
		if !os.SameFile(info, fdInfo) {
			logger.logFile.Close()

			logger.logFile, err = os.OpenFile(logger.logFilePath, os.O_CREATE|os.O_WRONLY, 0644)
			if err != nil {
				return fmt.Errorf("cannot open log file %s: %w", logger.logFilePath, err)
			}

			logger.currentSize = 0
			return nil
		}

		logger.currentSize = info.Size()

		// Check if the size exceeds the max log size
		if info.Size() > logger.maxLogFileSize {
			var renameErrMsg string
			destPath := logger.logFilePath + ".old"

			if _, err := os.Stat(destPath); err == nil {
				// File exists, so try to remove it
				err = os.Remove(destPath)
				if err != nil {
					return fmt.Errorf("failed to remove old file '%s': %w", destPath, err)
				}
			} else if !os.IsNotExist(err) {
				// Some error other than "not exist" occurred
				return fmt.Errorf("error checking if file exists '%s': %w", destPath, err)
			}

			logger.logFile.Close()

			// Rename the current file as .old
			const maxRetries = 30
			const retryDelay = 100 * time.Millisecond

			var err error
			for i := 1; i <= maxRetries; i++ {
				err = os.Rename(logger.logFilePath, destPath)
				if err == nil {
					break
				}

				// Save last error
				renameErrMsg = err.Error()

				// Sleep before retry (except last attempt)
				if i < maxRetries {
					time.Sleep(retryDelay)
				}
			}

			if err != nil {
				// final failure after retries
				renameErrMsg = fmt.Sprintf("rename failed after %d retries: %v", maxRetries, err)
			}

			logger.logFile, err = os.OpenFile(logger.logFilePath, os.O_CREATE|os.O_WRONLY, 0644)
			if err != nil {
				// Both rename and reopening log file have failed
				message := "Cannot open log file " + logger.logFilePath
				if renameErrMsg != "" {
					message = fmt.Sprintf("%s and cannot rename it: %s", message, renameErrMsg)
				}

				return fmt.Errorf("%s", message)
			}

			// Reopening is successful, log file will be truncated
			if renameErrMsg != "" {

				// show renaming failed message in log
				// Prepare the formatting of the logs; date, pid, etc
				now := time.Now()
				timestamp := fmt.Sprintf("%.4d%.2d%.2d:%.2d%.2d%.2d.%03d",
					now.Year(),
					int(now.Month()),
					now.Day(),
					now.Hour(),
					now.Minute(),
					now.Second(),
					now.Nanosecond()/1000000,
				)

				pid := os.Getpid()

				// output log that failed to rename the file
				msg := fmt.Sprintf("[%s %s] %6d:%s cannot rename log file \"%s\" to \"%s\": %s\n", common.Manager.Name, CannotRenameMsgID, pid, timestamp, logger.logFilePath, destPath, renameErrMsg)
				_, err = fmt.Fprint(logger.logFile, msg)
				if err != nil {
					return fmt.Errorf("failed to write message to log file '%s': %w", logger.logFilePath, err)
				}

				// output log that log file is truncated
				msg = fmt.Sprintf("[%s %s] %6d:%s Logfile \"%s\" size reached configured limit LogFileSize. Renaming the logfile to \"%s\" and starting a new logfile failed. The logfile was truncated and started from beginning.\n", common.Manager.Name, TruncateMsgID, pid, timestamp, logger.logFilePath, destPath)
				_, err = fmt.Fprint(logger.logFile, msg)
				if err != nil {
					return fmt.Errorf("failed to write message to log file '%s': %w", logger.logFilePath, err)
				}
			}
		}

	}

	return nil
}

func ShouldIgnoreLog(msgLevel LogLevel) bool {
	return msgLevel > logger.logLevel
}

// JaJobLog logs a job-related message and inserts it into the ja_2_run_log_message database table.
//
// Parameters:
//   - messageId: a string identifier representing the log message of run_log_message
//   - data: a common.Logging struct containing necessary data to insert into ja_2_run_log_message
//
// Returns:
//   - error: returns an error if the log insertion fails, otherwise nil.
//
// This function is intended for job execution logs that need to be stored persistently.
//
// Example:
//
//	logData := common.Logging{
//			InnerJobnetMainID: 1234,
//			InnerJobnetID: 1234,
//	}
//	err := JaJobLog("JC00000007", logData)
func JaJobLog(messageId string, data Logging) error {
	var (
		innerJobnetMainId, innerJobnetId, innerJobId, updateDate                                                 uint64
		jobnetStatus, runType, publicFlag, jobType, sessionFlag, methodFlag, jobStatus, returnCode               int
		jobnetID, jobnetName, userName, jobId, jobexitCdValue, stdOutValue, stdErrValue, jobName, stdOut, stdErr string
		sqlFlag                                                                                                  int
		err                                                                                                      error
		eventData                                                                                                common.EventData
	)

	innerJobnetId = data.InnerJobnetID
	innerJobnetMainId = data.InnerJobnetMainID
	updateDate = data.UpdateDate
	jobnetStatus = int(data.JobnetStatus)
	runType = data.RunType
	publicFlag = data.PublicFlag
	jobnetID = data.JobnetID
	jobnetName = data.JobnetName
	userName = data.UserName

	innerJobId = data.InnerJobID
	methodFlag = int(data.MethodFlag)
	jobStatus = int(data.JobStatus)
	jobId = data.JobID
	jobName = data.JobName

	if innerJobnetId == 0 && innerJobId == 0 {
		return fmt.Errorf("missing innerJobnetId and innerJobId")
	}

	now := time.Now()
	logDate := now.Unix()

	sqlFlag = 0
	if messageId == common.JC_JOBNET_TIMEOUT {
		sqlFlag = 3
	} else if innerJobId != 0 && messageId != common.JC_JOBNET_TIMEOUT {

		sqlFlag = 1

		if messageId == common.JC_JOB_END || messageId == common.JC_JOB_ERR_END {

			jobType = int(data.JobType)

			if jobType == int(common.IconTypeJob) || jobType == int(common.IconTypeLess) {
				sqlFlag = 2
			}

			if jobType == int(common.IconTypeLess) {

				sessionFlag = data.SessionFlag
				if sessionFlag == common.JA_SES_OPERATION_FLAG_CLOSE {
					sqlFlag = 1
				}
			}

			if sqlFlag == 2 {
				count := 0
				// count from ja_2_run_value_table(after_variable _ array[])
				// counting
				jobexitCdValue = data.JobExitCDValue
				stdOutValue = data.StdOutValue
				stdErrValue = data.StdErrValue

				if jobexitCdValue == "JOB_EXIT_CD" && stdOutValue == "STD_OUT" && stdErrValue == "STD_ERR" {
					count = 3
				}

				if count != 3 {
					sqlFlag = 1
				}
			}
		}
	}

	eventData.Event.Name = common.EventInsertLogTable
	eventData.Event.UniqueKey = common.GetUniqueKey(common.LoggingProcess)
	eventData.NextProcess.Name = common.LoggingProcess
	var insertQuery string
	var parValue []any
	switch sqlFlag {

	case 1:

		insertQuery = "INSERT INTO ja_2_run_log_table ( log_date, inner_jobnet_id, inner_jobnet_main_id, inner_job_id, update_date, method_flag, jobnet_status, job_status, run_type, public_flag, jobnet_id, jobnet_name, job_id, job_name, user_name, message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
		parValue = []any{logDate, innerJobnetId, innerJobnetMainId, innerJobId, updateDate,
			methodFlag, jobnetStatus, jobStatus, runType, publicFlag, jobnetID, jobnetName,
			jobId, jobName, userName, messageId}

	case 2:
		returnCode = data.JobExitCD
		stdOut = data.StdOut
		stdErr = data.StdErr

		insertQuery = "INSERT INTO ja_2_run_log_table ( log_date, inner_jobnet_id, inner_jobnet_main_id, inner_job_id, update_date, method_flag, jobnet_status, job_status, run_type, public_flag, jobnet_id, jobnet_name, job_id, job_name, user_name, return_code, std_out, std_err, message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
		parValue = []any{logDate, innerJobnetId, innerJobnetMainId, innerJobId, updateDate,
			methodFlag, jobnetStatus, jobStatus, runType, publicFlag, jobnetID, jobnetName,
			jobId, jobName, userName, returnCode, stdOut, stdErr, messageId}

	case 3:
		insertQuery = "INSERT INTO ja_2_run_log_table ( log_date, inner_jobnet_id, inner_jobnet_main_id, update_date, method_flag, jobnet_status, job_status, run_type, public_flag, jobnet_id, jobnet_name, job_id, job_name, user_name, message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
		parValue = []any{logDate, innerJobnetId, innerJobnetMainId, updateDate,
			0, jobnetStatus, 0, runType, publicFlag, jobnetID, jobnetName,
			"", "", userName, messageId}

	default:
		// duplicate error jobnet deletion
		if messageId == common.JC_JOBNET_ERR_END {
			eventData.AddPQ("DELETE FROM ja_2_run_log_table WHERE inner_jobnet_id = ? AND message_id = ?",
				innerJobnetId, messageId)
			JaLog("JAJOBLOG000001", data, innerJobnetId)

			// prevent deadlock
			err = CreateNextEventForLogger(eventData, innerJobnetMainId, jobnetID, uint64(innerJobId))
			if err != nil {
				return fmt.Errorf("failed to create transaction file for delete query: %w", err)
			}

			// clear queued queries so next insert is independent
			eventData.ParameterizedQueries = nil
		}

		// Adding new log
		insertQuery = "INSERT INTO ja_2_run_log_table ( log_date, inner_jobnet_id, inner_jobnet_main_id, update_date, method_flag, jobnet_status, run_type, public_flag, jobnet_id, jobnet_name, user_name, message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
		parValue = []any{logDate, innerJobnetId, innerJobnetMainId, updateDate,
			methodFlag, jobnetStatus, runType, publicFlag, jobnetID, jobnetName,
			userName, messageId}
	}

	eventData.AddPQ(insertQuery, parValue...)
	err = CreateNextEventForLogger(eventData, innerJobnetMainId, jobnetID, uint64(innerJobId))
	if err != nil {
		return err
	}
	return nil
}

// JaLog logs a structured message using a message ID and optional formatted arguments.
//
// Parameters:
//   - messageId: a string identifier representing the message ID in logmessage_64BIT.txt
//   - data: a common.Logging struct that contains necessary data for inserting into ja_2_send_message_table
//   - varags: optional values used to format the message string (like fmt.Sprintf).
//     These will be applied to a format string looked up by messageId.
//
// Returns:
//   - error: returns an error if logging fails, otherwise nil.
//
// Example usage:
//
//	    logData := common.Logging{
//			InnerJobnetMainID: 1234,
//			InnerJobnetID: 1234,
//		}
//	err := JaLog("JA001", logData, 1234, "done")
func JaLog(messageID string, data Logging, varargs ...any) (string, error) {

	msg, err := WriteLog(messageID, varargs...)
	if err != nil {
		return "", err
	}

	if msg == nil {
		return "", nil
	}

	message := fmt.Sprintf(msg.Body, varargs...)
	fullMessage := fmt.Sprintf("[%s %s] %s", messageID, common.Manager.Name, message) // [MessageID ManagerName] MessageBody

	if msg.NoticeFlag == 0 {
		return fullMessage, nil
	}

	zbxSendInfo := common.ZbxSendInfo{
		InnerJobID:    data.InnerJobID,
		InnerJobnetID: data.InnerJobnetID,
		LogMessage:    message,
		LogMessageID:  messageID,
		LogLevel:      int(msg.LogLevel),
	}

	if err := CreateNotiEvent(zbxSendInfo); err != nil {
		return "", err
	}

	return fullMessage, nil
}

func CreateNotiEvent(zbxSendInfo common.ZbxSendInfo) error {
	// Prepare eventData
	var eventData common.EventData
	eventData.Event.Name = common.EventInsertSendMsg
	procType := common.LoggingProcess
	eventData.Event.UniqueKey = common.GetUniqueKey(procType)
	eventData.NextProcess.Name = common.ProcessType(procType)
	eventData.NextProcess.Data = zbxSendInfo

	// Write to file
	err := CreateNextEventForLogger(eventData, zbxSendInfo.InnerJobnetID, "", zbxSendInfo.InnerJobID)
	if err != nil {
		WriteLog("JALOGGER200003", err)
		return err
	}

	return nil
}

func CreateNextEventForLogger(data common.EventData, innerJobnetId uint64, jobnetId string, innerJobId uint64) error {
	funcName := "CreateNextEventForLogger"

	result := eventcore.CreateEventCore(&data, innerJobnetId, jobnetId, innerJobId)

	if result.Err != nil {
		return result.Err
	}

	WriteLog("JAUTILS000001", funcName, data.Event.Name, data.Transfer.Files[0].Source)

	if result.UDSSuccess {
		WriteLog("JAUTILS400001", funcName, data.Event.Name, innerJobnetId, innerJobId)
	} else {
		WriteLog("JAUTILS300001", funcName, data.Event.Name, innerJobnetId, innerJobId, result.UDSErrMsg)
	}

	return nil
}
