/*
** 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"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"plugin"
	"regexp"
	"runtime"
	"runtime/debug"
	"slices"
	"strconv"
	"strings"
	"time"

	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/agent"
	"jobarranger2/src/libs/golibs/config_reader/conf"
	"jobarranger2/src/libs/golibs/config_reader/server"
	"jobarranger2/src/libs/golibs/database"
	"jobarranger2/src/libs/golibs/forker"
	jatcp "jobarranger2/src/libs/golibs/ja_tcp"
	"jobarranger2/src/libs/golibs/logger/logger"
	"jobarranger2/src/libs/golibs/uds"
	"jobarranger2/src/libs/golibs/utils"
	wm "jobarranger2/src/libs/golibs/worker_manager"

	"github.com/fsnotify/fsnotify"
)

var (
	arg      args
	db       database.Database
	goPlugin *plugin.Plugin
	dataChan chan common.Data
)

var (
	versionNo  = "7.2.0"
	revisionNo = "7777"
)

func initializeManager(goPluginId int) (common.ManagerInfo, bool) {
	var manager common.ManagerInfo

	switch goPluginId {
	case 1:
		manager.Name = common.NotificationManagerProcess
		manager.PluginPath = common.NotificationManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.NotificationManagerFolder)
		manager.SubFolders = common.NotificationManagerSubFolders
		manager.WatchFolders = common.NotificationManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.NotificationManagerSockFile)
		manager.DBMaxConCount = server.Options.NotificationManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = true
	case 2:
		manager.Name = common.ZabbixLinkManagerProcess
		manager.PluginPath = common.ZabbixLinkManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.ZabbixLinkManagerFolder)
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.ZabbixLinkManagerSockFile)
		manager.DBMaxConCount = server.Options.ZabbixLinkManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = true
	case 3:
		manager.Name = common.IconResultManagerProcess
		manager.PluginPath = common.IconResultManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.IconResultManagerFolder)
		manager.SubFolders = common.IconResultManagerSubFolders
		manager.WatchFolders = common.IconResultManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.IconResultManagerSockFile)
		manager.DBMaxConCount = server.Options.IconResultManagerMaxDBConCount
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = true
	case 4:
		manager.Name = common.DBSyncerManagerProcess
		manager.PluginPath = common.DBSyncerManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.DBSyncerManagerFolder)
		manager.SubFolders = common.DBSyncerManagerSubFolders
		manager.WatchFolders = common.DBSyncerManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.DBSyncerManagerSockFile)
		manager.DBMaxConCount = server.Options.DBSyncerManagerMaxDBConCount
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = true
	case 5:
		manager.Name = common.RecoveryManagerProcess
		manager.PluginPath = common.RecoveryManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.RecoveryManagerFolder)
		manager.SubFolders = common.RecoveryManagerSubFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.RecoveryManagerSockFile)
		manager.DBMaxConCount = server.Options.RecoveryManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = false
	case 6:
		manager.Name = common.IconExecManagerProcess
		manager.PluginPath = common.IconExecManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.IconExecManagerFolder)
		manager.SubFolders = common.IconExecManagerSubFolders
		manager.WatchFolders = common.IconExecManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.IconExecManagerSockFile)
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = false
	case 7:
		manager.Name = common.FlowManagerProcess
		manager.PluginPath = common.FlowManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.FlowManagerFolder)
		manager.SubFolders = common.FlowManagerSubFolders
		manager.WatchFolders = common.FlowManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.FlowManagerSockFile)
		manager.DBMaxConCount = server.Options.FlowManagerMaxDBConCount
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = true
	case 8:
		manager.Name = common.JobnetManagerProcess
		manager.PluginPath = common.JobnetManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.JobnetManagerFolder)
		manager.SubFolders = common.JobnetManagerSubFolders
		manager.WatchFolders = common.JobnetManagerWatchFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.JobnetManagerSockFile)
		manager.DBMaxConCount = server.Options.JobnetManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = true
	case 9:
		manager.Name = common.TimerManagerProcess
		manager.PluginPath = common.TimerManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.TimerManagerFolder)
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.TimerManagerSockFile)
		manager.DBMaxConCount = server.Options.TimerManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = false
	case 10:
		manager.Name = common.TrapperManagerProcess
		manager.PluginPath = common.TrapperManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.TrapperManagerFolder)
		manager.SubFolders = common.TrapperManagerSubFolders
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.TrapperManagerSockFile)
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = false
	case 11:
		manager.Name = common.HousekeeperManagerProcess
		manager.PluginPath = common.HousekeeperManagerPluginPath
		manager.FolderPath = filepath.Join(server.Options.TmpDir, common.HousekeeperManagerFolder)
		manager.SockFilePath = filepath.Join(server.Options.UnixSockParentDir, common.HousekeeperManagerSockFile)
		manager.DBMaxConCount = server.Options.HousekeeperManagerMaxDBConCount
		manager.DBDaemonFlag = true
		manager.DBOneTimeFlag = false
	case 12:
		manager.Name = common.AgentManagerProcess
		manager.PluginPath = common.AgentManagerPluginPath
		manager.FolderPath = filepath.Join(agent.Options.TmpDir, common.AgentManagerFolder)
		manager.SubFolders = common.AgentManagerSubFolders
		manager.WatchFolders = common.AgentManagerWatchFolders
		manager.SockFilePath = ""
		manager.DBDaemonFlag = false
		manager.DBOneTimeFlag = false
	default:
		return manager, common.Fail
	}
	return manager, common.Succeed
}

func getInFilePathFromEvent(eventData common.EventData) string {
	if len(eventData.Transfer.Files) == 0 {
		return ""
	}

	source := eventData.Transfer.Files[0].Source
	dest := eventData.Transfer.Files[0].Destination

	// Choose whichever contains manager name
	if strings.Contains(source, string(common.Manager.Name)) {
		return source
	}
	if strings.Contains(dest, string(common.Manager.Name)) {
		return dest
	}

	return ""
}

func startUniqueKeyCleaner() {
	workerId := "startUniqueKeyCleaner"

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerId)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- workerId
		}

		all := wm.GetAllUniqueKeys()

		for key, filePath := range all {
			if _, err := os.Stat(filePath); os.IsNotExist(err) {
				// File is gone → remove key
				wm.RemoveUniqueKey(key)
				logger.JaLog("JAFRAMEWORK400011", logger.Logging{}, common.Manager.Name, key, filePath)
			}
		}

		time.Sleep(time.Second * 60)
	}
}

func monitorHeartbeats(cMonitorPid int) {
	const workerId = "monitorHeartbeats"

	// catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			logger.WriteLog("JAFRAMEWORK100002", common.Manager.Name, workerId, string(debug.Stack()))
		}
	}()

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerId)
			return
		case <-time.After(10 * time.Second):
			sendHeartbeatToCmonitor(cMonitorPid)
		}

		heartbeats := wm.GetHeartbeatMapCopy()

		for id, lastHeartbeat := range heartbeats {

			worker, ok := wm.GetWorker(id)
			if !ok {
				// Worker exited. skip heartbeat monitor
				continue
			}

			if worker.Timeout == 0 {
				continue
			}

			if time.Since(lastHeartbeat) <= time.Duration(worker.Timeout)*time.Second {
				// No timeout
				continue
			}

			// Daemon worker timeout detected
			if worker.WorkerType == 0 {
				logger.JaLog(
					"JAFRAMEWORK200010",
					logger.Logging{},
					common.Manager.Name,
					id,
					lastHeartbeat.Format("2006-01-02 15:04:05"),
					time.Now().Format("2006-01-02 15:04:05"),
					time.Since(lastHeartbeat).Seconds(),
					worker.Timeout,
				)

				exitProcess()
				return
			}

			// One time worker timeout detected
			logger.JaLog("JAFRAMEWORK300003", logger.Logging{}, common.Manager.Name, id)
			wm.RemoveWorker(id)
		}
	}
}

func registerHeartbeats() {
	const workerId = "registerHeartbeats"

	// catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			logger.WriteLog("JAFRAMEWORK100002", common.Manager.Name, workerId, string(debug.Stack()))
		}
	}()

	for id := range wm.Wm.MonitorChan {
		workerExists := wm.CheckWorkerExist(id)

		if !workerExists {
			continue
		}

		wm.RegisterHeartbeat(id)
		logger.JaLog("JAFRAMEWORK400005", logger.Logging{}, common.Manager.Name, id)
	}
}

func tcpListener(hostname string, listenPort int, allowedIPs []string) {
	const workerId = "tcpListener"
	connChan := make(chan *common.NetConnection)

	// Create Tcp server
	tcpSock, err := jatcp.CreateTcpServer(hostname, listenPort, allowedIPs)
	if err != nil {
		logger.JaLog("JAFRAMEWORK200017", logger.Logging{}, common.Manager.Name, err)
		os.Exit(1)
	}
	defer tcpSock.Close()

	// Goroutine to accept incoming connections
	go func() {
		for {
			conn, err := tcpSock.Accept()
			if err != nil {
				// Exit goroutine if socket closed due to process stop
				if errors.Is(err, net.ErrClosed) && wm.ProcessExitFlag {
					return
				}

				logger.JaLog("JAFRAMEWORK200018", logger.Logging{}, common.Manager.Name, err)
				time.Sleep(100 * time.Millisecond)
				continue
			}
			connChan <- conn
		}
	}()

	// Main processing loop
	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerId)
			return
		case conn := <-connChan:
			// Pass connection to parent worker
			wm.StartWorker(func() {
				receivedData, err := receiveTcpSocketData(conn)
				if receivedData == nil {
					logger.JaLog("JAFRAMEWORK400021", logger.Logging{}, common.Manager.Name, receivedData)
					return
				}

				if err != nil {
					logger.JaLog("JAFRAMEWORK200019", logger.Logging{}, common.Manager.Name, receivedData, err)
				}

				logger.JaLog("JAFRAMEWORK400022", logger.Logging{}, common.Manager.Name, receivedData)

			}, "tcp"+strconv.FormatInt(time.Now().UnixNano(), 10), 60, 2, string(common.Manager.Name))

			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId

		case <-time.After(60 * time.Second):
			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId
		}
	}
}

func unixDomainListener(unixSocketFilePath string) {
	const workerId = "unixDomainListener"
	connChan := make(chan *common.NetConnection)

	// Remove existing socket file if it exists
	if _, err := os.Stat(unixSocketFilePath); err == nil {
		if err := os.Remove(unixSocketFilePath); err != nil {
			logger.JaLog("JAFRAMEWORK200013", logger.Logging{}, common.Manager.Name, err)
			os.Exit(1)
		}
	}

	unixSocketFolder := filepath.Dir(unixSocketFilePath)
	if err := os.MkdirAll(unixSocketFolder, 0755); err != nil {
		logger.JaLog("JAFRAMEWORK200028", logger.Logging{}, unixSocketFolder, common.Manager.Name, err)
		os.Exit(1)
	}

	// Create Unix domain socket server
	unixSock, err := uds.CreateUdsServer(unixSocketFilePath)
	if err != nil {
		logger.JaLog("JAFRAMEWORK200014", logger.Logging{}, common.Manager.Name, err)
		os.Exit(1)
	}
	defer unixSock.Close()

	// Goroutine to accept incoming connections
	go func() {
		for {
			conn, err := unixSock.Accept()
			if err != nil {
				// Exit goroutine if socket closed due to process stop
				if errors.Is(err, net.ErrClosed) && wm.ProcessExitFlag {
					return
				}

				logger.JaLog("JAFRAMEWORK200015", logger.Logging{}, common.Manager.Name, err)
				time.Sleep(100 * time.Millisecond)
				continue
			}
			connChan <- conn
		}
	}()

	// Main processing loop
	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerId)
			return
		case conn := <-connChan:
			// Pass connection to parent worker
			wm.StartWorker(func() {
				receivedData, err := receiveUdsSocketData(conn)

				if err != nil {
					logger.JaLog("JAFRAMEWORK200016", logger.Logging{}, common.Manager.Name, receivedData, err)
				}

				logger.JaLog("JAFRAMEWORK400023", logger.Logging{}, common.Manager.Name, receivedData)

			}, "uds"+strconv.FormatInt(time.Now().UnixNano(), 10), 60, 2, string(common.Manager.Name))

			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId

		case <-time.After(60 * time.Second):
			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId
		}
	}
}

func inotifyListener(watchFolderPath, pattern string, watchEvents fsnotify.Op) {
	const workerID = "inotifyListener"

	// Initialize fsnoify watcher
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.JaLog("JAFRAMEWORK200020", logger.Logging{}, common.Manager.Name, err)
		return
	}
	defer watcher.Close()

	entries, err := os.ReadDir(watchFolderPath)
	if err != nil {
		logger.JaLog("JAFRAMEWORK200021", logger.Logging{}, common.Manager.Name, watchFolderPath, err)
		return
	}

	// Add subdirectories to watcher
	for _, entry := range entries {
		if entry.IsDir() && slices.Contains(common.Manager.WatchFolders, entry.Name()) {
			subdirPath := filepath.Join(watchFolderPath, entry.Name())

			if err := watcher.Add(subdirPath); err != nil {
				logger.JaLog("JAFRAMEWORK200022", logger.Logging{}, common.Manager.Name, subdirPath, err)
			} else {
				logger.JaLog("JAFRAMEWORK400007", logger.Logging{}, common.Manager.Name, subdirPath)
			}
		}
	}

	regex, err := regexp.Compile(pattern)
	if err != nil {
		logger.JaLog("JAFRAMEWORK200023", logger.Logging{}, common.Manager.Name, pattern, err)
		return
	}

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerID)
			return

		case event, ok := <-watcher.Events:
			if !ok {
				return
			}

			// Match pattern and event type
			if event.Op&watchEvents != 0 && regex.MatchString(event.Name) {
				logger.JaLog("JAFRAMEWORK400008", logger.Logging{}, common.Manager.Name, event.Name)

				var fd *os.File
				var err error

				if runtime.GOOS != "windows" {
					fd, err = os.Open(event.Name)
					if err != nil {
						logger.JaLog("JAFRAMEWORK400018", logger.Logging{}, common.Manager.Name, event.Name, err)
						continue
					}
				} else {
					fd, err = utils.OpenFileWithLockRetry(event.Name, 5, utils.OpenOnly)
					if err != nil {
						logger.JaLog("JAFRAMEWORK200024", logger.Logging{}, common.Manager.Name, event.Name, err)
						continue
					}
				}

				// Pass event data to parent worker
				wm.StartWorker(func() {
					defer fd.Close()

					// Acquire a shared lock on the lock file before reading the file.
					file, err := utils.LockFile(utils.LOCKFILE_SHARE_LOCK | utils.LOCKFILE_FAIL_ON_LOCK)
					if err != nil {
						logger.JaLog("JAUTILS200003", logger.Logging{}, workerID, common.Manager.Name, "", "", err)
					}

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

					receivedData, err := readJsonFileData(fd)

					if len(receivedData) == 0 {
						logger.JaLog("JAFRAMEWORK400020", logger.Logging{}, common.Manager.Name, event.Name)
						return
					}

					if err != nil {
						logger.JaLog("JAFRAMEWORK400019", logger.Logging{}, common.Manager.Name, event.Name, err)
					}

					var mapData map[string]any
					if err := json.Unmarshal(receivedData, &mapData); err != nil {
						logger.JaLog("JAFRAMEWORK400024", logger.Logging{}, common.Manager.Name, event.Name, receivedData)
					} else {
						logger.JaLog("JAFRAMEWORK400024", logger.Logging{}, common.Manager.Name, event.Name, mapData)
					}

				}, "inotify"+strconv.FormatInt(time.Now().UnixNano(), 10), 60, 2, string(common.Manager.Name))
			}

			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerID

		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			logger.JaLog("JAFRAMEWORK300002", logger.Logging{}, common.Manager.Name, err)

		case <-time.After(60 * time.Second):
			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerID
		}
	}
}

func parentWorker() {
	const (
		workerId     = "parentWorker"
		maxQueueSize = 500
	)

	var (
		err              error
		processEventData func(data common.Data)
		priorityQueue    = make([]common.Data, 0)
		normalQueue      = make([]common.Data, 0)
	)

	// Load plugin function (for non-Windows)
	if runtime.GOOS != "windows" {
		processEventData, err = loadPluginFunc[func(data common.Data)](goPlugin, "ProcessEventData")
		if err != nil {
			logger.JaLog("JAFRAMEWORK200007", logger.Logging{}, common.Manager.Name, err)
			return
		}
	}

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAFRAMEWORK400004", logger.Logging{}, common.Manager.Name, workerId)
			return

		case data := <-dataChan:
			var eventData common.EventData

			// Parse event data if needed
			if data.EventRoute != common.TcpRoute && common.Manager.Name != common.ZabbixLinkManagerProcess {

				// Drop queues if total size exceeds limit
				totalQueueSize := len(priorityQueue) + len(normalQueue)
				if totalQueueSize >= maxQueueSize {
					logger.JaLog("JAFRAMEWORK000008", logger.Logging{}, common.Manager.Name, totalQueueSize, wm.GetUniqueKeyCount())
					priorityQueue = priorityQueue[:0]
					normalQueue = normalQueue[:0]

					// Clear unique keys
					wm.ClearAllUniqueKeys()
				}

				if err := json.Unmarshal(data.EventData, &eventData); err != nil {
					logger.JaLog("JAFRAMEWORK400025", logger.Logging{}, common.Manager.Name, workerId, data.EventRoute, data.EventFileName, string(data.EventData), err)
					continue
				}

				key := eventData.Event.UniqueKey

				// Check for unique key existence
				if wm.IsUniqueKeyExists(key) {
					// Cancel processing
					logger.JaLog("JAFRAMEWORK400006", logger.Logging{}, common.Manager.Name, data.EventRoute, key)
					continue
				}

				inFilePath := getInFilePathFromEvent(eventData)
				if inFilePath == "" {
					logger.JaLog("JAFRAMEWORK400012", logger.Logging{}, common.Manager.Name, data.EventRoute, eventData.Event.Name, key, inFilePath)
					continue
				}

				// Check file in the "in" directory
				if _, err := os.Stat(inFilePath); err != nil {
					// Cancel processing
					logger.JaLog("JAFRAMEWORK400015", logger.Logging{}, common.Manager.Name, data.EventRoute, eventData.Event.Name, key, inFilePath)
					continue
				}

				logger.JaLog("JAFRAMEWORK400013", logger.Logging{}, common.Manager.Name, data.EventRoute, eventData.Event.Name, key, inFilePath)

				// Register unique key
				wm.RegisterUniqueKey(key, inFilePath)
			}

			// Add to correct queue
			if eventData.Event.Priority {
				priorityQueue = append(priorityQueue, data)
			} else {
				normalQueue = append(normalQueue, data)
			}

			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId

		case <-time.After(1 * time.Second):
			// Send heartbeat to go monitor
			wm.Wm.MonitorChan <- workerId
		}

		// Process queued events
		for {
			var (
				nextData      common.Data
				nextEventData common.EventData
				inFilePath    string
			)

			if len(priorityQueue) > 0 {
				nextData = priorityQueue[0]
			} else if len(normalQueue) > 0 {
				nextData = normalQueue[0]
			} else {
				break
			}

			if err := json.Unmarshal(nextData.EventData, &nextEventData); err != nil {
				logger.JaLog("JAFRAMEWORK400025", logger.Logging{}, common.Manager.Name, workerId, nextData.EventRoute, nextData.EventFileName, string(nextData.EventData), err)

				// Remove event
				if len(priorityQueue) > 0 {
					priorityQueue = priorityQueue[1:]
				} else if len(normalQueue) > 0 {
					normalQueue = normalQueue[1:]
				}

				continue
			}

			// Check worker limits
			if wm.Wm.OnetimeLimitWorkerCount >= wm.Wm.OnetimeWorkerLimit {
				logger.JaLog("JAFRAMEWORK000001", logger.Logging{}, common.Manager.Name, wm.Wm.OnetimeWorkerLimit, wm.Wm.OnetimeLimitWorkerCount, workerId)
				break
			}

			inFilePath = getInFilePathFromEvent(nextEventData)

			if runtime.GOOS != "windows" {
				// Get DB connection if needed
				if common.Manager.DBOneTimeFlag {
					dbConn, err := db.GetConn()
					if err != nil {
						if err == database.ErrNoAvailableDBConn {
							logger.JaLog("JAFRAMEWORK000002", logger.Logging{}, common.Manager.Name, db.GetDBConfig().MaxConCount(), db.GetPoolSize(), workerId, nextEventData.Event.Name)
							break
						}
						logger.JaLog("JAFRAMEWORK200005", logger.Logging{}, common.Manager.Name, err)
						os.Exit(1)
					}
					nextData.DBConn = dbConn
				}

				// Check file in the "in" directory
				if nextData.EventRoute != common.TcpRoute && common.Manager.Name != common.ZabbixLinkManagerProcess {
					if _, err := os.Stat(inFilePath); err != nil {
						// Cancel processing
						logger.JaLog("JAFRAMEWORK400016", logger.Logging{}, common.Manager.Name, nextData.EventRoute, nextEventData.Event.Name, nextEventData.Event.UniqueKey, inFilePath)

						// Close db session
						if nextData.DBConn != nil {
							if err := nextData.DBConn.EndSession(); err != nil {
								logger.JaLog("JAFRAMEWORK200012", logger.Logging{}, common.Manager.Name, err)
								nextData.DBConn.Close()
							}
						}

						// Close uds connection
						if nextData.NetConn != nil {
							nextData.NetConn.Close()
						}

						// Remove event
						if len(priorityQueue) > 0 {
							priorityQueue = priorityQueue[1:]
						} else if len(normalQueue) > 0 {
							normalQueue = normalQueue[1:]
						}

						continue
					}
				}

				// Start one-time main worker
				workerFunc := func(nextData common.Data, nextEventData common.EventData, inFilePath string) {
					defer func() {
						// Close db session
						if nextData.DBConn != nil {
							if err := nextData.DBConn.EndSession(); err != nil {
								logger.JaLog("JAFRAMEWORK200012", logger.Logging{}, common.Manager.Name, err)
								nextData.DBConn.Close()
							}
						}

						// Close uds connection
						if nextData.NetConn != nil {
							nextData.NetConn.Close()
						}

						key := nextEventData.Event.UniqueKey

						if nextData.EventRoute != common.TcpRoute && common.Manager.Name != common.ZabbixLinkManagerProcess {
							// Check if the transaction file still exists
							if _, err := os.Stat(inFilePath); err == nil {
								// File still exists
								logger.JaLog("JAFRAMEWORK400017", logger.Logging{}, common.Manager.Name, nextData.EventRoute, nextEventData.Event.Name, nextEventData.Event.UniqueKey, inFilePath)
								return
							}

							// Remove unique key
							wm.RemoveUniqueKey(key)
							logger.JaLog("JAFRAMEWORK400014", logger.Logging{}, common.Manager.Name, nextData.EventRoute, nextEventData.Event.Name, nextEventData.Event.UniqueKey, inFilePath)
						}
					}()
					processEventData(nextData)
				}

				wm.StartWorker(
					func() { workerFunc(nextData, nextEventData, inFilePath) },
					"one-time-worker"+strconv.FormatInt(time.Now().UnixNano(), 10),
					60,
					2,
					string(common.Manager.Name),
				)

			} else {
				// Windows process execution
				// Get agent manager binary file path
				frameworkExecPath, err := os.Executable()
				if err != nil {
					logger.JaLog("JAFRAMEWORK200030", logger.Logging{}, common.Manager.Name, err)
					os.Exit(1)
				}
				frameworkExecPath = filepath.Clean(frameworkExecPath)
				agentManagerExecPath := filepath.Join(filepath.Dir(frameworkExecPath), common.AgentExecFileName)

				// Start one-time main worker
				workerFunc := func(nextData common.Data, nextEventData common.EventData, inFilePath string) {
					var cmd *exec.Cmd
					if cmd, err = runAgentManagerProcess(nextData, agentManagerExecPath, nextData.EventRoute); err != nil {
						logger.JaLog("JAFRAMEWORK200030", logger.Logging{}, common.Manager.Name, err)
						os.Exit(1)
					}

					defer func() {
						cmd.Wait()
						key := nextEventData.Event.UniqueKey
						if nextData.EventRoute != common.TcpRoute {
							// Check if the transaction file still exists
							if _, err := os.Stat(inFilePath); err == nil {
								// File still exists
								logger.JaLog("JAFRAMEWORK400017", logger.Logging{}, common.Manager.Name, nextData.EventRoute, nextEventData.Event.Name, nextEventData.Event.UniqueKey, inFilePath)
								return
							}

							// Remove unique key
							wm.RemoveUniqueKey(key)
							logger.JaLog("JAFRAMEWORK400014", logger.Logging{}, common.Manager.Name, nextData.EventRoute, nextEventData.Event.Name, nextEventData.Event.UniqueKey, inFilePath)
						}
					}()
				}

				wm.StartWorker(
					func() { workerFunc(nextData, nextEventData, inFilePath) },
					"one-time-worker"+strconv.FormatInt(time.Now().UnixNano(), 10),
					60,
					2,
					string(common.Manager.Name),
				)
			}

			// Remove processed event
			if len(priorityQueue) > 0 {
				priorityQueue = priorityQueue[1:]
			} else if len(normalQueue) > 0 {
				normalQueue = normalQueue[1:]
			}
		}
	}
}

func connectDB(dbPluginPath string, dbConf database.DBConfig) (database.Database, error) {
	dbPlugin, err := plugin.Open(dbPluginPath)
	if err != nil {
		return nil, err
	}

	symbol, err := dbPlugin.Lookup("NewDB")
	if err != nil {
		return nil, err
	}

	newDB, ok := symbol.(func(*database.DBConfig) (database.Database, error))
	if !ok {
		return nil, errors.New("type assertion failed")
	}

	return newDB(&dbConf)
}

func loadGoPlugin(goPluginPath string) (*plugin.Plugin, error) {
	goPlugin, err := plugin.Open(goPluginPath)
	if err != nil {
		return goPlugin, err
	}
	return goPlugin, err
}

func loadPluginFunc[T any](plugin *plugin.Plugin, symbolName string) (T, error) {
	symbol, err := plugin.Lookup(symbolName)
	if err != nil {
		var zero T
		return zero, fmt.Errorf("function '%s' not found: %w", symbolName, err)
	}

	fn, ok := symbol.(T)
	if !ok {
		var zero T
		return zero, fmt.Errorf("function '%s' has invalid signature", symbolName)
	}

	return fn, nil
}

func receiveUdsSocketData(conn *common.NetConnection) (map[string]any, error) {
	var err error
	var eventData map[string]any

	if eventData, err = conn.Receive(); err != nil {
		return eventData, err
	}

	content, err := json.Marshal(eventData)
	if err != nil {
		return eventData, err
	}

	dataChan <- common.Data{
		EventData:  content,
		NetConn:    conn,
		EventRoute: common.UdsRoute,
	}
	return eventData, nil
}

func receiveTcpSocketData(conn *common.NetConnection) (map[string]any, error) {
	var err error
	var eventData map[string]any

	if eventData, err = conn.Receive(); err != nil {
		//For port checks
		if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
			return nil, nil
		}

		return eventData, err
	}

	if eventData == nil {
		return eventData, err
	}

	content, err := json.Marshal(eventData)
	if err != nil {
		return eventData, err
	}

	dataChan <- common.Data{
		EventData:  content,
		NetConn:    conn,
		EventRoute: common.TcpRoute,
	}
	return eventData, nil
}

func readJsonFileData(file *os.File) ([]byte, error) {
	defer file.Close()
	data, err := io.ReadAll(file)
	if err != nil {
		return data, err
	}

	if len(data) == 0 {
		return data, nil
	}

	dataChan <- common.Data{
		EventData:     data,
		EventRoute:    common.InotifyRoute,
		EventFileName: file.Name(),
	}
	return data, nil
}

func createFolders(parentFolderPath string, subFolders []string) error {
	if parentFolderPath == "" {
		return fmt.Errorf("parent folder path is empty")
	}

	if err := os.MkdirAll(parentFolderPath, 0755); err != nil {
		return fmt.Errorf("failed to create parent folder %q: %w", parentFolderPath, err)
	}

	if len(subFolders) != 0 {
		for _, folder := range subFolders {
			fullPath := filepath.Join(parentFolderPath, folder)
			if err := os.MkdirAll(fullPath, 0755); err != nil {
				return fmt.Errorf("failed to create subfolder %q: %w", fullPath, err)
			}
		}
	}

	return nil
}

func startManagerDaemonWorkers() {
	var (
		frameworkExecPath    string
		agentManagerExecPath string
		err                  error
	)

	if runtime.GOOS != "windows" {
		startDaemonWorkers, err := loadPluginFunc[func(data common.Data)](goPlugin, "StartDaemonWorkers")
		if err != nil {
			logger.JaLog("JAFRAMEWORK200007", logger.Logging{}, common.Manager.Name, err)
			os.Exit(1)
		}

		data := common.Data{}
		if arg.useDB && common.Manager.DBDaemonFlag {
			data.DB = db
		}

		// start manager daemons
		startDaemonWorkers(data)
	} else {
		frameworkExecPath, err = os.Executable()
		if err != nil {
			logger.JaLog("JAFRAMEWORK200026", logger.Logging{}, common.Manager.Name, err)
			os.Exit(1)
		}
		frameworkExecPath = filepath.Clean(frameworkExecPath)
		agentManagerExecPath = filepath.Join(filepath.Dir(frameworkExecPath), common.AgentExecFileName)

		proc := forker.New(forker.ProcessData{
			ExecPath: agentManagerExecPath,
			ExecParams: []string{
				"-config-path", arg.configPath,
				"-daemon",
			},

			Detached:   false,
			DirectExec: true,
		})

		cmd, err := proc.StartNewProcess()
		if err != nil {
			logger.JaLog("JAFRAMEWORK200026", logger.Logging{}, common.Manager.Name, err)
			os.Exit(1)
		}

		go func() {
			if err = cmd.Wait(); err != nil {
				logger.JaLog("JAFRAMEWORK200029", logger.Logging{}, err)
			}

			logger.JaLog("JAFRAMEWORK100003", logger.Logging{})
			os.Exit(1)
		}()
	}
}

func loadPlugin() {
	if runtime.GOOS != "windows" {
		var err error
		goPlugin, err = loadGoPlugin(common.Manager.PluginPath)
		if err != nil {
			logger.JaLog("JAFRAMEWORK200006", logger.Logging{}, common.Manager.Name, err)
			os.Exit(1)
		}
	}
}

func startGCWorker(interval time.Duration) {
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()

		for range ticker.C {
			var m runtime.MemStats

			runtime.ReadMemStats(&m)
			before := m.Alloc

			// Force GC and memory release
			runtime.GC()
			debug.FreeOSMemory()

			runtime.ReadMemStats(&m)
			after := m.Alloc

			cleaned := int64(0)
			if before > after {
				cleaned = int64(before - after)
			}

			// JAFRAMEWORK INFO log
			logger.JaLog(
				"JAFRAMEWORK400026",
				logger.Logging{},
				before,
				after,
				cleaned,
			)
		}
	}()
}

func main() {

	var (
		hostname   string
		listenPort int
		ret        bool
		err        error
	)

	// argument processing
	arg, _ = parseArgs()
	checkArgs(arg)
	common.ConfigFilePath = arg.configPath

	if arg.targetServer == "server" {
		//load config
		if err = conf.Load(arg.configPath, &server.Options); err != nil {
			os.Exit(1)
		}

		//initialize logger
		if err := logger.InitLogger(
			server.Options.JaLogFile,
			server.Options.JaLogMessageFile,
			server.Options.LogType,
			server.Options.LogFileSize*1024*1024,
			server.Options.DebugLevel,
			logger.TargetTypeServer,
		); err != nil {
			fmt.Fprintf(os.Stderr, "failed to initialize logger: %v", err)
			os.Exit(1)
		}

	} else {
		//load config
		if err = conf.Load(arg.configPath, &agent.Options); err != nil {
			os.Exit(1)
		}

		//initialize logger
		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 {
			fmt.Fprintf(os.Stderr, "failed to initialize logger: %v", err)
			os.Exit(1)
		}

		if runtime.GOOS == "windows" {
			logger.JaLog("JAAGENT000001", logger.Logging{}, versionNo, revisionNo)
		}
	}

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

	//initialize go managers
	if common.Manager, ret = initializeManager(arg.goPluginId); !ret {
		logger.JaLog("JAFRAMEWORK200001", logger.Logging{}, arg.goPluginId)
		os.Exit(1)
	}

	//create parent folder and sub-folders
	if err = createFolders(common.Manager.FolderPath, common.Manager.SubFolders); err != nil {
		logger.JaLog("JAFRAMEWORK200002", logger.Logging{}, common.Manager.Name, err)
		os.Exit(1)
	}

	if arg.targetServer == "server" {
		startGCWorker(time.Duration(server.Options.GCInterval) * time.Minute)
		logger.JaLog("JAFRAMEWORK400001", logger.Logging{}, arg.cMonitorPid, arg.goPluginId, arg.useDB, arg.useTCP, arg.configPath, arg.targetServer, arg.help, arg.version)
		common.ServerTmpFolderPath = server.Options.TmpDir
		common.LockFilePath = filepath.Join(server.Options.TmpDir, "lock_file.lock")
		hostname = server.Options.ListenIP
		listenPort = server.Options.JaTrapperListenPort

		//connect db
		if arg.useDB {
			dbConf := database.NewDBConfig()
			if common.Manager.Name == common.ZabbixLinkManagerProcess {
				dbConf.SetDBHostname(server.Options.DBHost)
				dbConf.SetDBName(server.Options.DBName)
				dbConf.SetDBUser(server.Options.DBUser)
				dbConf.SetDBPasswd(server.Options.DBPassword)
				dbConf.SetDBPort(server.Options.DBPort)
				dbConf.SetTLSMode(server.Options.DBTLSConnect)
				dbConf.SetTLSCaFile(server.Options.DBTLSCAFile)
				dbConf.SetTLSCertFile(server.Options.DBTLSCertFile)
				dbConf.SetTLSKeyFile(server.Options.DBTLSKeyFile)
				dbConf.SetTLSCipher(server.Options.DBTLSCipher)
				dbConf.SetTLSCipher13(server.Options.DBTLSCipher13)
				dbConf.SetMaxConCount(common.Manager.DBMaxConCount)
			} else {
				dbConf.SetDBHostname(server.Options.JazDBHost)
				dbConf.SetDBName(server.Options.JazDBName)
				dbConf.SetDBUser(server.Options.JazDBUser)
				dbConf.SetDBPasswd(server.Options.JazDBPassword)
				dbConf.SetDBPort(server.Options.JazDBPort)
				dbConf.SetTLSMode(server.Options.JazDBTLSConnect)
				dbConf.SetTLSCaFile(server.Options.JazDBTLSCAFile)
				dbConf.SetTLSCertFile(server.Options.JazDBTLSCertFile)
				dbConf.SetTLSKeyFile(server.Options.JazDBTLSKeyFile)
				dbConf.SetTLSCipher(server.Options.JazDBTLSCipher)
				dbConf.SetTLSCipher13(server.Options.JazDBTLSCipher13)
				dbConf.SetMaxConCount(common.Manager.DBMaxConCount)
			}

			retryInterval := 10 * time.Second
			logInterval := 5 * time.Second
			lastLogTime := time.Now()

			switch server.Options.DBType {
			case database.MariaDBType:
				for {
					db, err = connectDB(server.Options.MariaDBPlugin, *dbConf)
					if err == nil {
						break
					}
					if lastLogTime.IsZero() || time.Since(lastLogTime) >= logInterval {
						logger.JaLog("JAFRAMEWORK200003", logger.Logging{}, common.Manager.Name, err)
						lastLogTime = time.Now()
					}
					time.Sleep(retryInterval)
				}
			case database.MysqlDBType:
				for {
					db, err = connectDB(server.Options.MysqlDBPlugin, *dbConf)
					if err == nil {
						break
					}
					if lastLogTime.IsZero() || time.Since(lastLogTime) >= logInterval {
						logger.JaLog("JAFRAMEWORK200003", logger.Logging{}, common.Manager.Name, err)
						lastLogTime = time.Now()
					}
					time.Sleep(retryInterval)
				}
			case database.PostgresDBType:
				for {
					db, err = connectDB(server.Options.PostgresDBPlugin, *dbConf)
					if err == nil {
						break
					}
					if lastLogTime.IsZero() || time.Since(lastLogTime) >= logInterval {
						logger.JaLog("JAFRAMEWORK200003", logger.Logging{}, common.Manager.Name, err)
						lastLogTime = time.Now()
					}
					time.Sleep(retryInterval)
				}
			default:
				logger.JaLog("JAFRAMEWORK200004", logger.Logging{}, common.Manager.Name, server.Options.DBType)
				os.Exit(1)
			}
		}

	} else {
		startGCWorker(time.Duration(agent.Options.GCInterval) * time.Minute)
		logger.JaLog("JAFRAMEWORK400002", logger.Logging{}, arg.cMonitorPid, arg.goPluginId, arg.useDB, arg.useTCP, arg.configPath, arg.targetServer)
		common.AgentTmpFolderPath = agent.Options.TmpDir
		common.LockFilePath = filepath.Join(agent.Options.TmpDir, "lock_file.lock")
		hostname = agent.Options.ListenIP
		listenPort = agent.Options.JaListenPort
	}

	// initialize worker manager
	dataChan = make(chan common.Data, 100)
	wm.InitializeWorkerManager(500)
	loadPlugin()

	// start the monitoring daemon workers
	go registerHeartbeats()
	go monitorHeartbeats(arg.cMonitorPid)

	if runtime.GOOS != "windows" {
		wm.StartWorker(monitorPPID, "monitorPPID", 180, 0, string(common.Manager.Name))
		wm.StartWorker(collectZombieChildren, "collectZombieChildren", 180, 0, string(common.Manager.Name))
	}

	// start the communication daemon workers
	wm.StartWorker(parentWorker, "parentWorker", 180, 0, string(common.Manager.Name))

	if common.Manager.SockFilePath != "" {
		wm.StartWorker(func() { unixDomainListener(common.Manager.SockFilePath) }, "unixDomainListener", 180, 0, string(common.Manager.Name))
	}

	if len(common.Manager.WatchFolders) != 0 {
		wm.StartWorker(func() { inotifyListener(common.Manager.FolderPath, `.*\.json$`, fsnotify.Create) }, "inotifyListener", 180, 0, string(common.Manager.Name))
	}

	if arg.useTCP {
		wm.StartWorker(func() { tcpListener(hostname, listenPort, nil) }, "tcpListener", 180, 0, string(common.Manager.Name))
	}

	// start the manager daemon workers
	startManagerDaemonWorkers()

	wm.StartWorker(startUniqueKeyCleaner, "startUniqueKeyCleaner", 180, 0, string(common.Manager.Name))

	// send process start signal to c main process
	switch arg.goPluginId {
	case 1, 3, 4, 5:
		sendSignal()
	}

	// wait for process stop signal
	waitStop(arg.cMonitorPid)
}
