/*
** 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 (
	"fmt"
	"io/fs"
	"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/event"
	"jobarranger2/src/libs/golibs/logger/logger"
	"jobarranger2/src/libs/golibs/utils"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"
)

const (
	FlowFolder   = "flow"
	JobnetFolder = "jobnet"
)

var targetMngFolders = map[string][]string{
	common.JobnetManagerFolder:       {common.InFolder, common.RunFolder, common.WaitFolder},
	common.FlowManagerFolder:         {common.InFolder, common.WaitFolder},
	common.IconExecManagerFolder:     {common.InFolder, common.RunFolder, common.ClientDataFolder},
	common.IconResultManagerFolder:   {common.InFolder, common.PendingFolder},
	common.NotificationManagerFolder: {common.InFolder},
	common.DBSyncerManagerFolder:     {common.InFolder},
}

type EndBeginIconPair struct {
	EndInnerJobID   uint64            `json:"end_inner_job_id"`
	InnerJobnetID   uint64            `json:"inner_jobnet_id"`
	EndIconType     common.IconType   `json:"end_job_type"`
	Status          common.StatusType `json:"status"`
	BeginInnerJobID uint64            `json:"begin_inner_job_id"`
	BeginIconType   common.IconType   `json:"begin_job_type"`
}

type ReExecuteAction string

const (
	ActionReExecute    ReExecuteAction = "re-execute"
	ActionSubJobnetEnd ReExecuteAction = "subjobnet-end"
	ActionJobnetRun    ReExecuteAction = "jobnet-run"
	ActionSendEndEvent ReExecuteAction = "send-end-event"
	ActionChangeRunErr ReExecuteAction = "runerr"
	ActionDoNothing    ReExecuteAction = "do-nothing"
)

type MoveDecision int

const (
	Keep MoveDecision = iota
	Move
	Drop
)

var rerunFolder = filepath.Join(common.RecoveryManagerFolder, common.ReRunFolder) // Relative path
var rerunFolderFullPath = filepath.Join(common.ServerTmpFolderPath, rerunFolder)

var rerunFlowFolder = filepath.Join(rerunFolder, FlowFolder) // Relative path
var rerunFlowFullPath = filepath.Join(common.ServerTmpFolderPath, rerunFlowFolder)

var rerunJobnetFolder = filepath.Join(rerunFolder, JobnetFolder) // Relative path
var rerunJobnetFullPath = filepath.Join(common.ServerTmpFolderPath, rerunJobnetFolder)

var createdEvents []string

func reExecute(db database.Database) {
	fn := "reExecute"
	logData := logger.Logging{}
	createdEvents = []string{}

	logger.JaLog("JARECOVERYREEXEC000001", logData, fn)

	dbCon, err := db.GetConn()
	if err != nil {
		logger.JaLog("JARECOVERYREEXEC200001", logData, fn, err.Error())
		return
	}

	defer dbCon.EndSession()

	// Delete run_timeout_table for ext icons
	if err := deleteExtIconTimeout(dbCon); err != nil {
		logger.JaLog("JARECOVERYREEXEC200002", logData, fn, err.Error())
		return
	}

	// Clean the rerun folder
	if err := removeAllInDir(rerunFolderFullPath); err != nil {
		logger.JaLog("JARECOVERYREEXEC200003", logData, fn, rerunFolderFullPath, err.Error())
	}

	logger.JaLog("JARECOVERYREEXEC400005", logData, fn, rerunFolderFullPath)

	// Create sub folders of rerun: flow and jobnet
	createRerunSubFolders()

	// Obtain event files under managers
	eventFilesMap, err := gatherAllManagersEventFiles() // {manager: [files]}
	if err != nil {
		logger.JaLog("JARECOVERYREEXEC200006", logData, fn, err.Error())
		return
	}

	// Construct common.EventFileNameParts struct from event files for later use
	eventFiles := constructEventFileNameParts(eventFilesMap)

	logger.JaLog("JARECOVERYREEXEC400013", logData, fn, len(eventFiles))

	eventFiles, err = removeEventFilesForRanJobnets(dbCon, eventFiles)
	if err != nil {
		logger.JaLog("JARECOVERYREEXEC200010", logData, fn, err.Error())
	}

	// Fetch records from ja_2_run_jobnet_table for status check of jobnets
	eventFiles, err = processRunJobnets(dbCon, eventFiles)
	if err != nil {
		logger.JaLog("JARECOVERYREEXEC200027", logData, fn, err.Error())
	}

	// Move all event files that do not exist in database to end folder
	moveEventFiles(eventFiles)

	// Move all files for event creation
	flowManagerInFolder := filepath.Join(common.ServerTmpFolderPath, common.FlowManagerFolder, common.InFolder)
	if err := moveAllFiles(rerunFlowFullPath, flowManagerInFolder); err != nil {
		logger.JaLog("JARECOVERYREEXEC200029", logData, fn, rerunFlowFullPath, flowManagerInFolder, err.Error())
	}

	logger.JaLog("JARECOVERYREEXEC000009", logData, fn, rerunFlowFullPath, flowManagerInFolder)

	jobnetManagerInFolder := filepath.Join(common.ServerTmpFolderPath, common.JobnetManagerFolder, common.InFolder)
	if err := moveAllFiles(rerunJobnetFullPath, jobnetManagerInFolder); err != nil {
		logger.JaLog("JARECOVERYREEXEC200029", logData, fn, rerunFlowFullPath, jobnetManagerInFolder, err.Error())
	}

	logger.JaLog("JARECOVERYREEXEC000009", logData, fn, rerunFlowFullPath, jobnetManagerInFolder)

	logger.JaLog("JARECOVERYREEXEC000010", logData, fn, len(createdEvents), strings.Join(createdEvents, ","))

	// Delete hostlock flag files and re-create them
	if err := HandleHostLockFlagFiles(dbCon); err != nil {
		logger.JaLog("JARECOVERYREEXEC200033", logData, fn, err.Error())
		return
	}

}

func processRunJobnets(dbCon database.DBConnection, eventFiles []common.EventFileNameParts) ([]common.EventFileNameParts, error) {
	fn := "processRunJobnets"
	logData := logger.Logging{}

	logger.JaLog("JARECOVERYREEXEC400019", logData, fn)
	tableName := common.Ja2RunJobnetTable
	query := fmt.Sprintf("SELECT * FROM %s ORDER BY created_date", tableName)

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	dbResult, err := dbCon.Select(query)
	if err != nil {
		return eventFiles, fmt.Errorf("failed to select from %s: %v", tableName, err)
	}
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return eventFiles, fmt.Errorf("failed to fetch row from %s: %v", tableName, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)
		var runJobnetData common.RunJobnetTable
		err = utils.MapStringStringToStruct(row, &runJobnetData)
		if err != nil {
			return eventFiles, fmt.Errorf("failed to convert row to 'RunJobnetTable' struct [raw data=%v]: %v", row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobnetTable", runJobnetData)

		logger.JaLog("JARECOVERYREEXEC400021", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobnetData.Status, runJobnetData.InvoFlag)
		logData = logger.Logging{
			InnerJobnetID: runJobnetData.InnerJobnetID,
		}

		switch runJobnetData.Status {
		case common.JAJobnetStatusBegin:
			// move event files with found inner_jobnet_id to end folder of respective managers
			eventFiles = moveEventFilesByInnerJobnetID(eventFiles, runJobnetData.InnerJobnetID)

			// check if the jobnet information is loaded by checking ja_2_run_jobnet_summary_table
			runJobnetSummaryTable, err := getRunJobnetSummary(dbCon, runJobnetData.InnerJobnetID)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200008", logData, fn, common.Ja2RunJobnetSummaryTable, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
				continue
			}

			// summary data exists
			if runJobnetSummaryTable != nil {
				logger.JaLog("JARECOVERYREEXEC400022", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
				// change the invo flag to 1 if it is still 0
				if runJobnetData.InvoFlag == 0 {
					targetInvoFlag := 1
					err = updateInvoFlag(dbCon, runJobnetData.InnerJobnetID, targetInvoFlag)
					if err != nil {
						logger.JaLog("JARECOVERYREEXEC200009", logData, fn, targetInvoFlag, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
					}
				}

				// jobnet is loaded, send event file to jobnet manager run
				err = createJobnetLoadEvent(runJobnetData, *runJobnetSummaryTable, common.InFolder)
				if err != nil {
					logger.JaLog("JARECOVERYREEXEC200010", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
					continue
				}

				continue
			}

			// summary data does not exist
			logger.JaLog("JARECOVERYREEXEC400023", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

			// jobnet is yet to be loaded, change invo flag back to 0 if 1
			if runJobnetData.InvoFlag == 1 {
				targetInvoFlag := 0
				// invo flag is 1. change it back to 0 and let the loader do the work
				err = updateInvoFlag(dbCon, runJobnetData.InnerJobnetID, targetInvoFlag)
				if err != nil {
					logger.JaLog("JARECOVERYREEXEC200009", logData, fn, targetInvoFlag, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
					continue
				}
			}

			// invo flag is 0. let the loader do the work
			logger.JaLog("JARECOVERYREEXEC000004", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
			continue

		case common.JAJobnetStatusRun, common.JAJobnetStatusRunErr:
			if common.JaMainFlag(runJobnetData.MainFlag) == common.JA_JOBNET_MAIN_FLAG_MAIN {
				// fetch data from ja_2_run_jobnet_summary_table
				runJobnetSummaryTable, err := getRunJobnetSummary(dbCon, runJobnetData.InnerJobnetID)
				if err != nil {
					logger.JaLog("JARECOVERYREEXEC200008", logData, fn, common.Ja2RunJobnetSummaryTable, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
					continue
				}

				if runJobnetSummaryTable == nil {
					logger.JaLog("JARECOVERYREEXEC200011", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
					continue
				}

				// do not re-execute delayed jobnet
				if runJobnetSummaryTable.LoadStatus == common.JA_SUMMARY_LOAD_STATUS_DELAY {
					logger.JaLog("JARECOVERYREEXEC000012", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
					continue
				}

				// create the jobnet load transaction file
				err = createJobnetLoadEvent(runJobnetData, *runJobnetSummaryTable, common.RunFolder)
				if err != nil {
					logger.JaLog("JARECOVERYREEXEC200010", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
					continue
				}
			}

			excludeInnerJobIDs, err := processReadyRunIcon(dbCon, runJobnetData, eventFiles)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200010", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
			}

			// move event files with found inner_jobnet_id to end folder of respective managers excluding inner_job_ids
			eventFiles = moveEventFilesByInnerJobnetIDAndExcludeInnerJobIDs(eventFiles, runJobnetData.InnerJobnetID, excludeInnerJobIDs)

			// Get run flows
			runFlows, err := getRunFlowData(dbCon, runJobnetData.InnerJobnetID, 0)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200018", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err.Error())
			}

			if len(runFlows) > 0 {
				if err := processEndBeginIconPairs(dbCon, runJobnetData, runFlows); err != nil {
					logger.JaLog("JARECOVERYREEXEC200023", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
				}

				if err := processEndEndIcon(dbCon, runJobnetData); err != nil {
					logger.JaLog("JARECOVERYREEXEC200023", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
				}
			}

			if runJobnetData.Status == common.JAJobnetStatusRun {
				// find runerr icons to change the jobnet status
				if err := processRunerrIcon(dbCon, logData, runJobnetData); err != nil {
					logger.JaLog("JARECOVERYREEXEC200031", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
				}
			}

			if err := processAbortedIcon(dbCon, logData, runJobnetData); err != nil {
				logger.JaLog("JARECOVERYREEXEC200026", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
			}

			if err := processAbortedJobnet(dbCon, logData, runJobnetData); err != nil {
				logger.JaLog("JARECOVERYREEXEC200030", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
			}

			continue
		case common.JAJobnetStatusEnd, common.JAJobnetStatusEndErr:
			// move event files with found inner_jobnet_id to end folder of respective managers
			eventFiles = moveEventFilesByInnerJobnetID(eventFiles, runJobnetData.InnerJobnetID)
		default:
			logger.JaLog("JARECOVERYREEXEC200032", logData, fn, runJobnetData.Status, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)
		}

	}

	logger.JaLog("JARECOVERYREEXEC400058", logData, fn)

	return eventFiles, nil
}

func getRunJobnetSummary(dbCon database.DBConnection, innerJobnetID uint64) (*common.RunJobnetSummaryTable, error) {
	fn := "getRunJobnetSummary"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
	}

	tableName := common.Ja2RunJobnetSummaryTable
	query := fmt.Sprintf(`
		SELECT *
		FROM %s 
		WHERE inner_jobnet_id = %d
	`, tableName, innerJobnetID)
	query = strings.Join(strings.Fields(query), " ") // flatten query

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)
	dbResult, err := dbCon.Select(query)
	if err != nil {
		return nil, fmt.Errorf("failed to select from %s: %v", tableName, err)
	}
	defer dbResult.Free()

	if !dbResult.HasNextRow() {
		logger.JaLog("JARECOVERYREEXEC400020", logData, fn, tableName, innerJobnetID)
		return nil, nil
	}

	row, err := dbResult.Fetch()
	if err != nil {
		return nil, fmt.Errorf("failed to fetch row from %s: %v", tableName, err)
	}

	var runJobnetSummary common.RunJobnetSummaryTable
	err = utils.MapStringStringToStruct(row, &runJobnetSummary)
	if err != nil {
		return nil, fmt.Errorf("failed to convert row to 'RunJobnetSummaryTable' struct [raw data=%v]: %v", row, err)
	}

	logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobnetSummaryTable", runJobnetSummary)

	return &runJobnetSummary, nil
}

func updateInvoFlag(dbCon database.DBConnection, innerJobnetID uint64, targetInvoFlag int) error {
	fn := "updateInvoFlag"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
	}

	logger.JaLog("JARECOVERYREEXEC400024", logData, fn, targetInvoFlag, innerJobnetID)

	if err := dbCon.Begin(); err != nil {
		return fmt.Errorf("in %s(), failed to begin transaction: %v", fn, err)
	}

	tableName := common.Ja2RunJobnetTable
	query := fmt.Sprintf(`
		UPDATE %s
		SET invo_flag = %d
		WHERE inner_jobnet_id = %d
	`, tableName, targetInvoFlag, innerJobnetID)
	query = strings.Join(strings.Fields(query), " ") // flatten query

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	_, err := dbCon.Execute(query)
	if err := handleDBExecErr(err, dbCon, fn, query, logData); err != nil {
		return err
	}

	logger.JaLog("JARECOVERYREEXEC400025", logData, fn, targetInvoFlag, innerJobnetID)
	return nil
}

func updateEndCount(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64, endCount int) error {
	fn := "updateEndCount"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	logger.JaLog("JARECOVERYREEXEC400041", logData, fn, endCount, innerJobnetID, innerJobID)

	query := fmt.Sprintf(`
		UPDATE ja_2_run_job_table
		SET end_count = %d
		WHERE inner_job_id = %d
	`, endCount, innerJobID)
	query = strings.Join(strings.Fields(query), " ") // flatten query

	if err := dbCon.Begin(); err != nil {
		return fmt.Errorf("in %s(), failed to begin transaction: %v", fn, err)
	}

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	_, err := dbCon.Execute(query)
	if err := handleDBExecErr(err, dbCon, fn, query, logData); err != nil {
		return err
	}

	logger.JaLog("JARECOVERYREEXEC400042", logData, fn, endCount, innerJobnetID, innerJobID)

	return nil
}

// find event files by target inner_jobnet_id and move those files to end folder of respective managers
// return remaining event files slice
func moveEventFilesByInnerJobnetID(files []common.EventFileNameParts, targetInnerJobnetID uint64) []common.EventFileNameParts {
	fn := "moveEventFilesByInnerJobnetID"

	logger.JaLog("JARECOVERYREEXEC400017", logger.Logging{}, fn, targetInnerJobnetID, len(files))
	beforeCount := len(files)

	files = moveFiles(files, func(f common.EventFileNameParts) MoveDecision {
		if f.InnerJobnetID == targetInnerJobnetID {
			return Move
		}

		return Keep
	})

	logger.JaLog("JARECOVERYREEXEC400018", logger.Logging{}, fn, targetInnerJobnetID, beforeCount-len(files))

	return files
}

// find event files by target inner_jobnet_id and move those files to end folder of respective managers
// return remaining event files slice
func moveEventFilesByInnerJobnetIDAndExcludeInnerJobIDs(files []common.EventFileNameParts, targetInnerJobnetID uint64, excludeInnerJobIDs []uint64) []common.EventFileNameParts {
	fn := "moveEventFilesByInnerJobnetIDAndExcludeInnerJobIDs"

	logger.JaLog("JARECOVERYREEXEC400061", logger.Logging{}, fn, targetInnerJobnetID, excludeInnerJobIDs, len(files))
	beforeCount := len(files)

	files = moveFiles(files, func(f common.EventFileNameParts) MoveDecision {
		// 1. Drop excluded InnerJobIDs
		if slices.Contains(excludeInnerJobIDs, f.InnerJobID) {
			return Drop
		}

		// 2. Move files that match the target jobnet
		if f.InnerJobnetID == targetInnerJobnetID {
			return Move
		}

		// 3. Keep everything else
		return Keep
	})

	logger.JaLog("JARECOVERYREEXEC400062", logger.Logging{}, fn, targetInnerJobnetID, excludeInnerJobIDs, beforeCount-len(files))

	return files
}

// move event files to end folder of respective managers
// return remaining event files slice
func moveEventFiles(files []common.EventFileNameParts) {
	moveFiles(files, nil)
}

// Helper function to move files conditionally
func moveFiles(files []common.EventFileNameParts, filter func(f common.EventFileNameParts) MoveDecision) []common.EventFileNameParts {
	fn := "moveFiles"

	ensureManagersEndFolder()

	n := 0
	for _, f := range files {
		decision := Move
		if filter != nil {
			decision = filter(f)
		}

		switch decision {
		case Keep:
			// Keep file if filter returns false
			files[n] = f
			n++
			continue
		case Drop:
			continue
		case Move:
			// Move file to end folder
			dest := filepath.Join(server.Options.TmpDir, f.ManagerName, common.EndFolder, filepath.Base(f.FilePath))
			if err := os.Rename(f.FilePath, dest); err != nil {
				logger.JaLog("JARECOVERYREEXEC200028", logger.Logging{}, fn, f.FilePath, dest, utils.ErrMsgWithErrno(err))
				continue
			}

			logger.JaLog("JARECOVERYREEXEC400059", logger.Logging{}, fn, f.FilePath, dest)
		}

	}

	return files[:n]
}

func ensureManagersEndFolder() {
	fn := "ensureManagersEndFolder"

	for mngFolder, _ := range targetMngFolders {
		dir := filepath.Join(server.Options.TmpDir, mngFolder, common.EndFolder)

		info, err := os.Stat(dir)
		if err == nil && info.IsDir() {
			// Directory already exists
			logger.JaLog("JARECOVERYREEXEC400006", logger.Logging{}, fn, dir)
			continue
		}

		if err := os.MkdirAll(dir, common.FilePermission); err != nil {
			logger.JaLog("JARECOVERYREEXEC200004", logger.Logging{}, fn, dir, utils.ErrMsgWithErrno(err))
			continue
		}

		logger.JaLog("JARECOVERYREEXEC400007", logger.Logging{}, fn, dir)
	}
}

func createRerunSubFolders() {
	fn := "createRerunSubFolders"
	subFolders := []string{rerunFlowFullPath, rerunJobnetFullPath}

	for _, subFolder := range subFolders {
		info, err := os.Stat(subFolder)
		if err == nil && info.IsDir() {
			// Directory already exists
			logger.JaLog("JARECOVERYREEXEC400006", logger.Logging{}, fn, subFolder)
			continue
		}

		// create if not exists
		if err := os.MkdirAll(subFolder, common.FilePermission); err != nil {
			logger.JaLog("JARECOVERYREEXEC200004", logger.Logging{}, fn, subFolder, err.Error())
			continue
		}

		logger.JaLog("JARECOVERYREEXEC400007", logger.Logging{}, fn, subFolder)

	}
}

func constructEventFileNameParts(eventFilesMap map[string][]string) []common.EventFileNameParts {
	fn := "constructEventFileNameParts"
	logger.JaLog("JARECOVERYREEXEC400011", logger.Logging{}, fn)

	var eventFiles []common.EventFileNameParts
	for manager, transacFiles := range eventFilesMap {
		for _, transacFile := range transacFiles {
			parts, err := utils.ParseTransactionFileName(transacFile)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200007", logger.Logging{}, fn, transacFile, err.Error())
				continue
			}
			parts.ManagerName = manager

			eventFiles = append(eventFiles, *parts)
		}
	}

	logger.JaLog("JARECOVERYREEXEC400012", logger.Logging{}, fn)

	return eventFiles
}

func gatherAllManagersEventFiles() (map[string][]string, error) {
	fn := "gatherAllManagersEventFiles"

	var eventFiles []string
	eventFileMap := make(map[string][]string)

	for mngFolder, subFolders := range targetMngFolders {
		eventFiles = []string{}
		for _, subFolder := range subFolders {
			dir := filepath.Join(server.Options.TmpDir, mngFolder, subFolder)
			// create if not exists
			if err := os.MkdirAll(dir, common.FilePermission); err != nil {
				logger.JaLog("JARECOVERYREEXEC200004", logger.Logging{}, fn, dir, utils.ErrMsgWithErrno(err))
				continue
			}

			logger.JaLog("JARECOVERYREEXEC400008", logger.Logging{}, fn, dir)

			err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
				if err != nil {
					// Handle errors accessing files/directories
					logger.JaLog("JARECOVERYREEXEC200005", logger.Logging{}, fn, path, err.Error())
					return nil // continue walking
				}

				// Look for files with extension .json
				if !d.IsDir() && filepath.Ext(path) == ".json" {
					logger.JaLog("JARECOVERYREEXEC400009", logger.Logging{}, fn, path)
					eventFiles = append(eventFiles, path)
				}
				return nil
			})

			if err != nil {
				return eventFileMap, fmt.Errorf("failed to walk dir %s: %v", dir, err)
			}
		}

		eventFileMap[mngFolder] = eventFiles
	}

	logger.JaLog("JARECOVERYREEXEC400010", logger.Logging{}, fn)

	return eventFileMap, nil
}

// this function processes for re-exeuction of icons with status ready or run. it also processes start and end icon for special cases
func processReadyRunIcon(dbCon database.DBConnection, runJobnetData common.RunJobnetTable, eventFiles []common.EventFileNameParts) ([]uint64, error) {
	fn := "processReadyRunIcon"
	logData := logger.Logging{
		InnerJobnetID: runJobnetData.InnerJobnetID,
	}
	excludeInnerJobIDs := []uint64{}

	logger.JaLog("JARECOVERYREEXEC400036", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	// fetch run job data (status: ready, run OR job_type: start, end)
	tableName := common.Ja2RunJobTable
	query := fmt.Sprintf(`
			SELECT * FROM %s
			WHERE inner_jobnet_id = %d 
			AND (status IN (%d, %d) OR job_type IN (%d, %d))
		`, tableName, runJobnetData.InnerJobnetID, common.StatusReady, common.StatusRun, common.IconTypeStart, common.IconTypeEnd)
	query = strings.Join(strings.Fields(query), " ") // flatten query
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)
	dbResult, err := dbCon.Select(query)
	if err != nil {
		return excludeInnerJobIDs, fmt.Errorf("failed to select from %s: %v", tableName, err)
	}
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return excludeInnerJobIDs, fmt.Errorf("failed to fetch row from %s: %v", tableName, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)
		var runJobData common.RunJobTable
		err = utils.MapStringStringToStruct(row, &runJobData)
		if err != nil {
			return excludeInnerJobIDs, fmt.Errorf("failed to convert row to 'RunJobTable' struct [raw data=%v]: %v", row, err)
		}

		logData.InnerJobID = runJobData.InnerJobID
		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobTable", runJobnetData)

		// If process still exists, skip the re-execution of that icon
		logger.JaLog("JARECOVERYREEXEC400027", logData, fn, runJobData.ClientPID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
		info, err := utils.GetProcessInfo(runJobData.ClientPID)
		if err != nil {
			logger.JaLog("JARECOVERYREEXEC200012", logData, fn, runJobData.ClientPID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
			continue
		}

		if info != nil {
			logger.JaLog("JARECOVERYREEXEC400028", logData, fn, runJobData.ClientPID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, info)

			// check if it is jobarranger icon_client process
			if strings.Contains(info.Cmdline, "jobarranger") && strings.Contains(info.Cmdline, "icon_client") {
				excludeInnerJobIDs = append(excludeInnerJobIDs, runJobData.InnerJobID)
				logger.JaLog("JARECOVERYREEXEC000005", logData, fn, info.Cmdline, info.PID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
				continue
			}

			logger.JaLog("JARECOVERYREEXEC400029", logData, fn, info.Cmdline, info.PID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
		}

		// Check if the transaction file exists to see if process ends successfully or not
		execInDir := filepath.Join(server.Options.TmpDir, common.IconExecManagerFolder, common.InFolder)
		hasCompleted := false
		for _, f := range eventFiles {
			if f.InnerJobID != runJobData.InnerJobID {
				continue
			}

			// check if the file exists under /iconexecmanager/in
			relPath, err := filepath.Rel(execInDir, f.FilePath)
			if err != nil {
				continue // Not under execInDir
			}

			if !strings.HasPrefix(relPath, "..") {
				hasCompleted = true
				break
			}
		}

		// icon_client process has completed successfully as we found the event file
		if hasCompleted {
			excludeInnerJobIDs = append(excludeInnerJobIDs, runJobData.InnerJobID)
			logger.JaLog("JARECOVERYREEXEC000011", logData, fn, runJobData.ClientPID, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
			continue
		}

		// Jobnet Icon: 1 - re-execute, 2; check jobnet_status
		var subJobnetData *common.RunJobnetTable
		if runJobData.IconType == common.IconTypeJobnet {
			logger.JaLog("JARECOVERYREEXEC400031", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)

			subJobnetData, err = getRunJobnetDataByInnerJobID(dbCon, runJobData.InnerJobID)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200013", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, err.Error())
				continue
			}

			if subJobnetData == nil {
				logger.JaLog("JARECOVERYREEXEC200014", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
				continue
			}

			logger.JaLog("JARECOVERYREEXEC400032", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)
		}

		var action ReExecuteAction

		action, err = getActionOnIconType(runJobData, runJobnetData, subJobnetData, dbCon)
		if err != nil {
			logger.JaLog("JARECOVERYREEXEC200015", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType, err.Error())
			continue
		}

		if err := performAction(action, runJobData, runJobnetData, subJobnetData, dbCon); err != nil {
			logger.JaLog("JARECOVERYREEXEC200016", logData, fn, action, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType, err.Error())
			continue
		}

	}

	logger.JaLog("JARECOVERYREEXEC400037", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	return excludeInnerJobIDs, nil
}

// process for icons with end status and next icon at begin status
func processEndBeginIconPairs(dbCon database.DBConnection, runJobnetData common.RunJobnetTable, runFlows []common.RunFlowTable) error {
	fn := "processEndBeginIconPairs"
	logData := logger.Logging{
		InnerJobnetID: runJobnetData.InnerJobnetID,
	}

	logger.JaLog("JARECOVERYREEXEC400038", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	endBeginPairs, err := getEndBeginIconPairs(dbCon, runJobnetData.InnerJobnetID)
	if err != nil {
		return fmt.Errorf("failed to get end-begin pairs: %v", err)
	}

	resetMTEIcons := map[uint64]struct{}{}

	// Send end event for icon with end status in the end-begin icon pair
	for _, pair := range endBeginPairs {
		logger.JaLog("JARECOVERYREEXEC400039", logData, fn, pair)

		// check if the icon with status begin is multiple-end icon
		if pair.BeginIconType == common.IconTypeW {
			// check if this multiple-end icon already been processed
			if _, exists := resetMTEIcons[pair.BeginInnerJobID]; !exists {
				// reset the count for multiple end
				logger.JaLog("JARECOVERYREEXEC400040", logData, fn, runJobnetData.InnerJobnetID, pair.BeginInnerJobID)

				if err := updateEndCount(dbCon, pair.InnerJobnetID, pair.BeginInnerJobID, 0); err != nil {
					logger.JaLog("JARECOVERYREEXEC200019", logData, fn, runJobnetData.InnerJobnetID, pair.BeginInnerJobID)
					continue
				}

				logger.JaLog("JARECOVERYREEXEC000008", logData, fn, runJobnetData.InnerJobnetID, pair.BeginInnerJobID)

				// Mark as seen
				resetMTEIcons[pair.BeginInnerJobID] = struct{}{}
			} else {
				// Already processed → skip
				logger.JaLog("JARECOVERYREEXEC400044", logData, fn, runJobnetData.InnerJobnetID, pair.BeginInnerJobID)
			}
		}

		// IF-icon is a special case where it needs to be re-exeucted even if it has ended
		if pair.EndIconType == common.IconTypeIf {
			// check the status of the other next icon after IF-icon
			otherJobData, err := getOtherNextIconData(dbCon, pair.InnerJobnetID, pair.EndInnerJobID, pair.BeginInnerJobID)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200020", logData, fn, runJobnetData.InnerJobnetID, pair.EndInnerJobID)
				continue
			}

			// pair.EndInnerJobID - IF icon
			// pair.BeginInnerJobID - icon at BEGIN status
			// otherJobData.InnerJobID - other icon at ? status

			// Not in a loop job
			allInSameLoop := IsAllInSameLoop([]uint64{pair.EndInnerJobID, pair.BeginInnerJobID, otherJobData.InnerJobID}, runFlows)
			noneInLoop := IsNoneInLoop([]uint64{pair.EndInnerJobID, pair.BeginInnerJobID, otherJobData.InnerJobID}, runFlows)

			if (allInSameLoop || noneInLoop) && otherJobData.Status != common.StatusBegin {
				logger.JaLog("JARECOVERYREEXEC400048", logData, fn, pair.InnerJobnetID, pair.EndInnerJobID, otherJobData.InnerJobID)
				continue
			}

			if otherJobData.Status == common.StatusEnd {
				// IF-icon and BEGIN
				ifAndBeginStatusIconInLoop := IsAllInSameLoop([]uint64{pair.EndInnerJobID, pair.BeginInnerJobID}, runFlows)

				// IF-icon and END
				ifAndEndStatusIconInLoop := IsAllInSameLoop([]uint64{pair.EndInnerJobID, otherJobData.InnerJobID}, runFlows)

				if ifAndBeginStatusIconInLoop && !ifAndEndStatusIconInLoop {
					// Do not run IF-icon
					logger.JaLog("JARECOVERYREEXEC400049", logData, fn, pair.InnerJobnetID, pair.EndInnerJobID, otherJobData.InnerJobID)
					continue
				}

				if !ifAndBeginStatusIconInLoop && ifAndEndStatusIconInLoop {
					// this falls under end-end processing in a loop
					logger.JaLog("JARECOVERYREEXEC400050", logData, fn, pair.InnerJobnetID, pair.EndInnerJobID, otherJobData.InnerJobID)
					continue
				}

			}

			if otherJobData.Status != common.StatusBegin {
				logger.JaLog("JARECOVERYREEXEC400051", logData, fn, pair.InnerJobnetID, pair.EndInnerJobID, otherJobData.InnerJobID, otherJobData.Status)
				continue
			}

			// Both statuses after IF-icon are BEGIN, create rerun event for IF-icon
			if err := createIconRerunEvent(pair.InnerJobnetID, pair.EndInnerJobID, runJobnetData.JobnetID); err != nil {
				logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventIconRerun, pair.InnerJobnetID, runJobnetData.JobnetID, pair.EndInnerJobID, err.Error())
				continue
			}

			continue
		}

		// get run job variable table data
		runJobVariableData, err := getRunJobVariables(dbCon, runJobnetData.InnerJobnetID, pair.EndInnerJobID)
		if err != nil {
			logger.JaLog("JARECOVERYREEXEC200022", logData, fn, pair.InnerJobnetID, runJobnetData.JobnetID, pair.EndInnerJobID, err.Error())
			continue
		}

		// Send end icon event for other icon types
		if err := createIconResultEndEvent(pair, *runJobVariableData, runJobnetData); err != nil {
			logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventIconResultEnd, pair.InnerJobnetID, runJobnetData.JobnetID, pair.EndInnerJobID, err.Error())
			continue
		}

	}

	logger.JaLog("JARECOVERYREEXEC400052", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	return nil
}

// processing icons with end time greater than the next icon where both icons are at end status
func processEndEndIcon(dbCon database.DBConnection, runJobnetData common.RunJobnetTable) error {
	fn := "processEndEndIcon"
	logData := logger.Logging{
		InnerJobnetID: runJobnetData.InnerJobnetID,
	}

	logger.JaLog("JARECOVERYREEXEC400053", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	lastIcons, err := getIconsWithGreaterEndTime(dbCon, runJobnetData.InnerJobnetID)
	if err != nil {
		return fmt.Errorf("failed to get icons with end time greater than the next icon: %v", err)
	}

	for _, lastIcon := range lastIcons {
		logger.JaLog("JARECOVERYREEXEC400054", logData, fn, lastIcon)

		if lastIcon.IconType == common.IconTypeIf {
			nextIcons, err := getNextIcons(dbCon, lastIcon.InnerJobnetID, lastIcon.InnerJobID)
			if err != nil {
				logger.JaLog("JARECOVERYREEXEC200024", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, lastIcon.InnerJobID, err.Error())
				continue
			}

			shouldSkip := false
			for _, nextIcon := range nextIcons {
				// if both next icons are not at END status, skip
				if nextIcon.Status != common.StatusBegin && nextIcon.Status != common.StatusEnd {
					logger.JaLog("JARECOVERYREEXEC400055", logData, fn, lastIcon.InnerJobnetID, lastIcon.InnerJobID, nextIcon.InnerJobID, nextIcon.Status)
					shouldSkip = true
					break
				}

				// if end time of IF-icon is not greater than both next icons, skip
				if nextIcon.EndTime > lastIcon.EndTime {
					logger.JaLog("JARECOVERYREEXEC400056", logData, fn, lastIcon.InnerJobnetID, lastIcon.InnerJobID, nextIcon.InnerJobID, nextIcon.EndTime)
					shouldSkip = true
					break
				}
			}

			if shouldSkip {
				continue
			}

			if err := createIconRerunEvent(runJobnetData.InnerJobnetID, lastIcon.InnerJobID, runJobnetData.JobnetID); err != nil {
				logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventIconRerun, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, lastIcon.InnerJobID, err.Error())
				continue
			}

			continue
		}

		// get run job variable table data
		lastIconRunJobVariableData, err := getRunJobVariables(dbCon, runJobnetData.InnerJobnetID, lastIcon.InnerJobID)
		if err != nil {
			logger.JaLog("JARECOVERYREEXEC200022", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, lastIcon.InnerJobID, err.Error())
			continue
		}

		// Send icon result end event for other icon types
		iconPair := EndBeginIconPair{
			EndInnerJobID: lastIcon.InnerJobID,
			EndIconType:   lastIcon.IconType,
			InnerJobnetID: runJobnetData.InnerJobnetID,
			Status:        lastIcon.Status,
		}

		if err := createIconResultEndEvent(iconPair, *lastIconRunJobVariableData, runJobnetData); err != nil {
			logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventIconResultEnd, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, iconPair.EndInnerJobID, err.Error())
			continue
		}

	}

	logger.JaLog("JARECOVERYREEXEC400057", logData, fn, runJobnetData.InnerJobnetID, runJobnetData.JobnetID)

	return nil
}

// find any runerr icon to create jobnet runerr event to change the status of the jobnet from run to runerr
func processRunerrIcon(dbCon database.DBConnection, logData logger.Logging, runJobnetData common.RunJobnetTable) error {
	fn := "processRunerrIcon"

	// fetch run job data (status: runerr)
	query := fmt.Sprintf(`
			SELECT * FROM ja_2_run_job_table
			WHERE inner_jobnet_id = %d 
			AND status = %d LIMIT 1
		`, runJobnetData.InnerJobnetID, common.StatusRunErr)
	query = strings.Join(strings.Fields(query), " ") // flatten query
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)
	dbResult, err := dbCon.Select(query)
	if err != nil {
		return fmt.Errorf("failed to select query %q: %v", query, err)
	}
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return fmt.Errorf("failed to fetch row for query %q: %v", query, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)
		var runJobData common.RunJobTable
		err = utils.MapStringStringToStruct(row, &runJobData)
		if err != nil {
			return fmt.Errorf("failed to convert row to 'RunJobTable' struct [raw data=%v]: %v", row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobTable", runJobnetData)

		// create jobnet runerr event if the jobnet is not already at runerr status
		if err := createJobnetRunErrorEvent(runJobData.InnerJobnetID, runJobData.InnerJobID, runJobnetData.JobnetID); err != nil {
			logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventJobnetRunError, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, err.Error())
			continue
		}

	}

	return nil
}

// resend icon stop event for the icons with abort status
func processAbortedIcon(dbCon database.DBConnection, logData logger.Logging, runJobnetData common.RunJobnetTable) error {
	fn := "processAbortedIcon"

	// fetch run job data (status: ready, run OR job_type: start, end)
	query := fmt.Sprintf(`
			SELECT * FROM ja_2_run_job_table
			WHERE inner_jobnet_id = %d 
			AND status = %d
		`, runJobnetData.InnerJobnetID, common.StatusAbort)
	query = strings.Join(strings.Fields(query), " ") // flatten query
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)
	dbResult, err := dbCon.Select(query)
	if err != nil {
		return fmt.Errorf("failed to select query %q: %v", query, err)
	}
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return fmt.Errorf("failed to fetch row for query %q: %v", query, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)
		var runJobData common.RunJobTable
		err = utils.MapStringStringToStruct(row, &runJobData)
		if err != nil {
			return fmt.Errorf("failed to convert row to 'RunJobTable' struct [raw data=%v]: %v", row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobTable", runJobnetData)

		// send abort event to flow manager
		if err := createIconStopEvent(runJobData.InnerJobnetID, runJobData.InnerJobID, runJobnetData.JobnetID); err != nil {
			logger.JaLog("JARECOVERYREEXEC200021", logData, fn, common.EventIconStop, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, err.Error())
			continue
		}

	}

	return nil
}

// resend jobnet stop event for the jobnet with abort status
func processAbortedJobnet(dbCon database.DBConnection, logData logger.Logging, runJobnetData common.RunJobnetTable) error {
	fn := "processAbortedJobnet"

	// fetch run job data (status: ready, run OR job_type: start, end)
	query := fmt.Sprintf(`
			SELECT * FROM ja_2_run_jobnet_summary_table
			WHERE inner_jobnet_id = %d
		`, runJobnetData.InnerJobnetID)
	query = strings.Join(strings.Fields(query), " ") // flatten query
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)
	dbResult, err := dbCon.Select(query)
	if err != nil {
		return fmt.Errorf("failed to select query %q: %v", query, err)
	}
	defer dbResult.Free()

	var runJobnetSummaryData common.RunJobnetSummaryTable
	if dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return fmt.Errorf("failed to fetch row for query %q: %v", query, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)

		err = utils.MapStringStringToStruct(row, &runJobnetSummaryData)
		if err != nil {
			return fmt.Errorf("failed to convert row to 'RunJobnetSummaryTable' struct [raw data=%v]: %v", row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobnetSummaryTable", runJobnetData)

	}

	if runJobnetSummaryData.JobnetAbortFlag == 0 {
		return nil
	}

	// send abort event to jobnet manager
	if err := createJobnetStopEvent(runJobnetData.InnerJobnetID, 0, runJobnetData.JobnetID); err != nil {
		return fmt.Errorf("failed to create %s event for inner_jobnet_id [%d], jobnet_id [%s]: %v", common.EventJobnetStop, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, err)
	}

	return nil
}

func performAction(action ReExecuteAction, runJobData common.RunJobTable, runJobnetData common.RunJobnetTable, subJobnetData *common.RunJobnetTable, dbCon database.DBConnection) error {
	fn := "performAction"
	logData := logger.Logging{
		InnerJobnetID: runJobData.InnerJobnetID,
		InnerJobID:    runJobData.InnerJobID,
	}

	logger.JaLog("JARECOVERYREEXEC400034", logData, fn, action, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType)

	switch action {
	case ActionReExecute:
		if err := createIconRerunEvent(runJobData.InnerJobnetID, runJobData.InnerJobID, runJobnetData.JobnetID); err != nil {
			return fmt.Errorf("failed to create icon rerun event: %v", err)
		}

	case ActionChangeRunErr:
		err := createIconRunErrEvent(runJobData, runJobnetData, dbCon)
		if err != nil {
			return fmt.Errorf("failed to create icon runerr event: %v", err)
		}

		// Wait for the status change to runerr
		sleepSecond := 1
		status := runJobData.Status
		for {
			logger.JaLog("JARECOVERYREEXEC400035", logData, fn, sleepSecond, status, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType)
			time.Sleep(time.Duration(sleepSecond) * time.Second)
			status, err = getIconStatus(dbCon, runJobData.InnerJobnetID, runJobData.InnerJobID)
			if err != nil {
				return fmt.Errorf("failed to get icon status: %v", err)
			}

			if status == common.StatusRunErr || status == common.StatusEnd {
				break
			}

		}

		logger.JaLog("JARECOVERYREEXEC000006", logData, fn, status, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType)

	case ActionSubJobnetEnd:
		// Create subjobnet_status_change event for flow manager
		if err := createSubJobnetStatusChangeEvent(subJobnetData.InnerJobnetID, subJobnetData.InnerJobID, subJobnetData.JobnetID); err != nil {
			return fmt.Errorf("failed to create subjobnet status change event: %v", err)
		}

	case ActionJobnetRun:
		if err := createJobnetRunEvent(runJobnetData.InnerJobnetID, runJobnetData.JobnetID, runJobnetData.InnerJobID); err != nil {
			return fmt.Errorf("failed to create jobnet run event: %v", err)
		}
	case ActionDoNothing: // Do nothing
	default:
		return fmt.Errorf("invalid action %s", action)
	}

	logger.JaLog("JARECOVERYREEXEC000007", logData, fn, action, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, runJobData.JobID, runJobData.IconType)

	return nil
}

func getActionOnIconType(runJobData common.RunJobTable, runJobnetData common.RunJobnetTable, subJobnetData *common.RunJobnetTable, dbCon database.DBConnection) (ReExecuteAction, error) {
	fn := "getActionOnIconType"
	logData := logger.Logging{
		InnerJobnetID: runJobData.InnerJobnetID,
		InnerJobID:    runJobData.InnerJobID,
	}

	logger.JaLog("JARECOVERYREEXEC400033", logData, fn, runJobData.InnerJobnetID, runJobData.InnerJobID, runJobData.IconType)
	switch runJobData.IconType {
	// local icons
	case common.IconTypeStart:
		if runJobData.Status == common.StatusBegin {
			return ActionJobnetRun, nil
		}
		if runJobData.Status == common.StatusReady || runJobData.Status == common.StatusRun {
			return ActionReExecute, nil
		}
	case common.IconTypeEnd:
		if runJobData.Status == common.StatusReady || runJobData.Status == common.StatusRun {
			return ActionReExecute, nil
		}

		if runJobData.Status == common.StatusEnd {
			// check if all icons are end
			lastExecutedIcon, err := getLastExecutedIcon(dbCon, runJobData.InnerJobnetID)
			if err != nil {
				return ActionDoNothing, fmt.Errorf("failed to get last executed icon: %v", err)
			}

			if lastExecutedIcon == nil {
				logger.JaLog("JARECOVERYREEXEC400060", logData, fn, runJobData.InnerJobnetID, runJobnetData.JobnetID)
				return ActionDoNothing, nil
			}

			if lastExecutedIcon.InnerJobID == runJobData.InnerJobID {
				// end icon is the last icon executed, we will re-run this
				return ActionReExecute, nil
			}
		}
	case common.IconTypeJobnet:
		if subJobnetData == nil {
			return ActionDoNothing, fmt.Errorf("subjobnet data is nil")
		}

		if runJobData.Status == common.StatusReady || subJobnetData.Status == common.StatusBegin {
			return ActionReExecute, nil
		}

		if subJobnetData.Status == common.StatusEnd {
			return ActionSubJobnetEnd, nil
		}

	case common.IconTypeIf, common.IconTypeValue, common.IconTypeExtJob,
		common.IconTypeM, common.IconTypeW, common.IconTypeL, common.IconTypeCalc, common.IconTypeTask,
		common.IconTypeInfo, common.IconTypeIfEnd, common.IconTypeRel, common.IconTypeAbort, common.IconTypeLink:
		return ActionReExecute, nil
	// agent icons
	case common.IconTypeJob, common.IconTypeFWait, common.IconTypeReboot:
		if runJobData.Status == common.StatusReady {
			return ActionReExecute, nil
		}
	// agentless and fcopy icons
	case common.IconTypeLess, common.IconTypeFCopy:
		return ActionChangeRunErr, nil
	default:
		return ActionDoNothing, fmt.Errorf("invalid icon type: %d", runJobData.IconType)
	}

	return ActionDoNothing, nil
}

// helper function to create event at a specified location
func createEvent(eventData common.EventData, innerJobnetID uint64, jobnetID string, innerJobID uint64, targetFolder string) error {
	fn := "createEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	if err := builder.CreateJsonFileAt(
		&eventData, innerJobnetID, jobnetID, innerJobID, targetFolder,
	); err != nil {
		return fmt.Errorf("failed to create event [%s]: %v", eventData.Event.Name, err)
	}

	var filename string
	if len(eventData.Transfer.Files) > 0 {
		filename = filepath.Base(eventData.Transfer.Files[0].Source)
	}

	if filename == "" {
		logger.JaLog("JARECOVERYREEXEC300001", logData, fn, eventData.Event.Name, innerJobnetID, jobnetID, innerJobID)
	}

	eventFilePath := filepath.Join(targetFolder, filename)

	logger.JaLog("JARECOVERYREEXEC000003", logData, fn, eventData.Event.Name, innerJobnetID, jobnetID, innerJobID, eventFilePath)

	event := fmt.Sprintf("%s:%d:%d:%s", jobnetID, innerJobnetID, innerJobID, eventData.Event.Name)
	createdEvents = append(createdEvents, event)

	return nil
}

func createIconResultEndEvent(endBeginIconPair EndBeginIconPair, runJobVariableData common.RunJobVariableTable, runJobnetData common.RunJobnetTable) error {
	fn := "createIconResultEndEvent"
	logData := logger.Logging{
		InnerJobnetID: endBeginIconPair.InnerJobnetID,
		InnerJobID:    endBeginIconPair.EndInnerJobID,
	}

	targetEvent := common.EventIconResultEnd
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, endBeginIconPair.InnerJobnetID, runJobnetData.JobnetID, endBeginIconPair.EndInnerJobID)

	var eventData common.EventData
	eventData.Event.Name = common.EventIconResultEnd
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Data = common.FlowProcessData{
		InnerJobnetId: runJobnetData.InnerJobnetID,
		JobnetId:      runJobnetData.JobnetID,
		InnerJobId:    endBeginIconPair.EndInnerJobID,
		IconType:      endBeginIconPair.EndIconType,
		FlowType:      common.FlowNormal,
		Status:        endBeginIconPair.Status,
		Data: common.IconExecutionProcessData{
			RunJobVariableData: runJobVariableData,
		},
	}

	return createEvent(eventData, runJobnetData.InnerJobnetID, runJobnetData.JobnetID, endBeginIconPair.EndInnerJobID, rerunFlowFolder)

}

func createIconRerunEvent(innerJobnetID, innerJobID uint64, jobnetID string) error {
	fn := "createIconRerunEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventFailoverIconRerun
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = targetEvent
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.FlowManagerProcess
	eventData.NextProcess.Data = common.FlowProcessData{
		InnerJobnetId: innerJobnetID,
		InnerJobId:    innerJobID,
		JobnetId:      jobnetID,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunFlowFolder)
}

func createIconStopEvent(innerJobnetID, innerJobID uint64, jobnetID string) error {
	fn := "createIconStopEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventIconStop
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = targetEvent
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.FlowManagerProcess
	eventData.NextProcess.Data = common.FlowProcessData{
		InnerJobnetId: innerJobnetID,
		InnerJobId:    innerJobID,
		JobnetId:      jobnetID,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunFlowFolder)
}

func createJobnetStopEvent(innerJobnetID, innerJobID uint64, jobnetID string) error {
	fn := "createJobnetStopEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventJobnetStop
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = targetEvent
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.JobnetManagerProcess
	eventData.NextProcess.Data = common.JobnetRunData{
		InnerJobnetId: innerJobnetID,
		JobnetID:      jobnetID,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunJobnetFolder)

}

func createJobnetRunErrorEvent(innerJobnetID, innerJobID uint64, jobnetID string) error {
	fn := "createJobnetRunErrorEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventJobnetRunError
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = targetEvent
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.JobnetManagerProcess
	eventData.NextProcess.Data = common.JobnetRunData{
		InnerJobnetId: innerJobnetID,
		JobnetID:      jobnetID,
		Status:        common.StatusRunErr,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunJobnetFolder)

}

// send the event to notify the flow manager that subnet has ended
func createSubJobnetStatusChangeEvent(innerJobnetID, innerJobID uint64, jobnetID string) error {
	fn := "createSubJobnetStatusChangeEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventSubJobnetStatusChange
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = common.EventSubJobnetStatusChange
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.FlowManagerProcess
	eventData.NextProcess.Data = common.FlowProcessData{
		InnerJobnetId: innerJobnetID,
		Status:        common.StatusEnd,
		JobnetId:      jobnetID,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunFlowFolder)

}

// send the event to notify the flow manager to start the start icon
func createJobnetRunEvent(innerJobnetID uint64, jobnetID string, innerJobID uint64) error {
	fn := "createJobnetRunEvent"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	targetEvent := common.EventJobnetRun
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, innerJobnetID, jobnetID, innerJobID)

	var eventData common.EventData
	eventData.Event.Name = common.EventJobnetRun
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.FlowManagerProcess
	eventData.NextProcess.Data = common.FlowProcessData{
		InnerJobnetId: innerJobnetID,
		JobnetId:      jobnetID,
	}

	return createEvent(eventData, innerJobnetID, jobnetID, innerJobID, rerunFlowFolder)

}

func createIconRunErrEvent(runJobData common.RunJobTable, runJobnetData common.RunJobnetTable, dbCon database.DBConnection) error {
	// creates runerr event to resultmanager
	fn := "createIconRunErrEvent"
	logData := logger.Logging{
		InnerJobnetID: runJobData.InnerJobnetID,
		InnerJobID:    runJobData.InnerJobID,
	}

	targetEvent := common.EventJobnetRun
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID)

	// Fetch before variables
	runJobVariable, err := getRunJobVariables(dbCon, runJobData.InnerJobnetID, runJobData.InnerJobID)
	if err != nil {
		return fmt.Errorf("failed to get run job variable: %v", err)
	}

	// Preparation of event data
	var eventData common.EventData
	eventData.Event.Name = common.EventIconResultUpdate
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.IconResultManagerProcess
	eventData.Transfer.Files = []common.FileTransfer{}

	// prepare data for result manager for result determination
	var iconExecProcessData common.IconExecutionProcessData
	iconExecProcessData.JobResult.Message = "Failover Error"
	iconExecProcessData.JobResult.JobID = strconv.FormatUint(runJobData.InnerJobID, 10)
	iconType := runJobData.IconType
	if iconType == common.IconTypeJob || iconType == common.IconTypeFWait || iconType == common.IconTypeReboot || iconType == common.IconTypeFCopy {
		iconExecProcessData.JobResult.Result = common.JA_RESPONSE_FAIL
	} else {
		iconExecProcessData.JobResult.Result = common.JA_JOBRESULT_FAIL
	}

	iconExecProcessData.RunJobVariableData = *runJobVariable
	iconExecProcessData.RunJobnetData = runJobnetData
	iconExecProcessData.RunJobData = runJobData
	eventData.NextProcess.Data = iconExecProcessData

	// Create event
	if err := event.CreateNextEvent(eventData, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID); err != nil {
		return fmt.Errorf("failed to create %s event for result manager: %v", targetEvent, err)
	}

	logger.JaLog("JARECOVERYREEXEC000003", logData, fn, eventData.Event.Name, runJobData.InnerJobnetID, runJobnetData.JobnetID, runJobData.InnerJobID, "") // event file full path will be logged inside CreateNextEvent()

	return nil

}

func createJobnetLoadEvent(runJobnetTable common.RunJobnetTable, runJobnetSummaryTable common.RunJobnetSummaryTable, targetSubFolder string) error {
	fn := "createJobnetLoadEvent"
	logData := logger.Logging{
		InnerJobnetID: runJobnetTable.InnerJobnetID,
	}

	jobnetRunData := common.JobnetRunData{
		RunType:           runJobnetTable.RunType,
		ScheduledTime:     runJobnetTable.ScheduledTime,
		MultipleOptions:   runJobnetTable.MultipleStartUp,
		MainFlag:          common.JaMainFlag(runJobnetTable.MainFlag),
		Status:            runJobnetTable.Status,
		JobnetID:          runJobnetTable.JobnetID,
		JobnetName:        runJobnetTable.JobnetName,
		UserName:          runJobnetTable.UserName,
		InnerJobnetMainId: runJobnetTable.InnerJobnetMainID,
		InnerJobnetId:     runJobnetTable.InnerJobnetID,
		InnerJobId:        runJobnetTable.InnerJobID,
		StartPendingFlag:  runJobnetSummaryTable.StartPendingFlag,
		TimeoutRunType:    runJobnetSummaryTable.TimeoutRunType,
		LoadStatus:        runJobnetSummaryTable.LoadStatus,
		JobnetTimeout:     int64(runJobnetSummaryTable.JobnetTimeout),
		Timezone:          runJobnetSummaryTable.Timezone,
		UpdateDate:        runJobnetSummaryTable.UpdateDate,
		PublicFlag:        runJobnetTable.PublicFlag,
	}

	targetEvent := common.EventJobnetLoad
	logger.JaLog("JARECOVERYREEXEC400026", logData, fn, targetEvent, runJobnetTable.InnerJobnetID, runJobnetTable.JobnetID, runJobnetTable.InnerJobID)

	var eventData common.EventData
	eventData.Event.Name = targetEvent
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = common.JobnetManagerProcess
	eventData.NextProcess.Data = jobnetRunData

	// Create next event
	targetFolder := filepath.Join(common.JobnetManagerFolder, targetSubFolder)
	return createEvent(eventData, runJobnetTable.InnerJobnetID, runJobnetTable.JobnetID, runJobnetTable.InnerJobID, targetFolder)
}

func getRunJobVariables(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64) (*common.RunJobVariableTable, error) {
	fn := "getRunJobVariables"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	query := fmt.Sprintf(`
		SELECT *
		FROM %s 
		WHERE inner_jobnet_id = %d AND inner_job_id = %d
		ORDER BY seq_no desc LIMIT 1
	`, common.Ja2RunJobVariableTable, innerJobnetID, innerJobID)

	return fetchSingleRowAsStruct[common.RunJobVariableTable](dbCon, query, logData, fn)
}

func getRunFlowData(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64) ([]common.RunFlowTable, error) {
	fn := "getRunFlowData"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	var byInnerJobID string

	if innerJobID != 0 {
		byInnerJobID = fmt.Sprintf("AND start_inner_job_id = %d", innerJobID)
	}

	query := fmt.Sprintf(`
		SELECT *
		FROM ja_2_run_flow_table
		WHERE inner_jobnet_id = %d %s
	`, innerJobnetID, byInnerJobID)

	return fetchRowsAsStructs[common.RunFlowTable](dbCon, query, logData, fn)
}

// Get icons with end status where end time is greater than the end time of next icon
func getNextIcons(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64) ([]common.RunJobTable, error) {
	fn := "getNextIcons"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	query := fmt.Sprintf(`
		SELECT j.* FROM ja_2_run_job_table AS j
		JOIN ja_2_run_flow_table as f
			ON j.inner_jobnet_id = %d AND f.start_inner_job_id = %d
			AND f.end_inner_job_id = j.inner_job_id
	`, innerJobnetID, innerJobID)

	return fetchRowsAsStructs[common.RunJobTable](dbCon, query, logData, fn)
}

// Get icons with end status where end time is greater than the end time of next icon
func getIconsWithGreaterEndTime(dbCon database.DBConnection, innerJobnetID uint64) ([]common.RunJobTable, error) {
	fn := "getIconsWithGreaterEndTime"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
	}

	query := fmt.Sprintf(`
		SELECT j_start.*
		FROM ja_2_run_job_table j_start
		WHERE j_start.inner_jobnet_id = %d
			AND j_start.status = %d
			AND EXISTS(
			    SELECT 1
				FROM ja_2_run_flow_table f
				JOIN ja_2_run_job_table j_end
					ON j_end.inner_job_id = f.end_inner_job_id
					AND j_end.inner_jobnet_id = %d
					AND j_end.status = %d
					AND j_end.end_time <> 0
				WHERE f.start_inner_job_id = j_start.inner_job_id
					AND f.inner_jobnet_id = %d
					AND j_start.end_time > j_end.end_time

			)
	`, innerJobnetID, common.StatusEnd, innerJobnetID, common.StatusEnd, innerJobnetID)

	return fetchRowsAsStructs[common.RunJobTable](dbCon, query, logData, fn)
}

func getLastExecutedIcon(dbCon database.DBConnection, innerJobnetID uint64) (*common.RunJobTable, error) {
	fn := "getLastExecutedIcon"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
	}

	tableName := common.Ja2RunJobTable
	query := fmt.Sprintf(`
		SELECT *
		FROM %s WHERE inner_jobnet_id = %d AND status = %d
		ORDER BY end_time DESC
		LIMIT 1;
	`, tableName, innerJobnetID, common.StatusEnd)

	return fetchSingleRowAsStruct[common.RunJobTable](dbCon, query, logData, fn)
}

func getRunJobData(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64) (*common.RunJobTable, error) {
	fn := "getRunJobData"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	query := fmt.Sprintf(`
		SELECT *
		FROM ja_2_run_job_table 
		WHERE inner_jobnet_id = %d AND inner_job_id = %d
	`, innerJobnetID, innerJobID)

	return fetchSingleRowAsStruct[common.RunJobTable](dbCon, query, logData, fn)

}

func getIconStatus(dbCon database.DBConnection, innerJobnetID uint64, innerJobID uint64) (common.StatusType, error) {
	fn := "getIconStatus"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	query := fmt.Sprintf(`
        SELECT status
        FROM ja_2_run_job_table
        WHERE inner_jobnet_id = %d AND inner_job_id = %d
    `, innerJobnetID, innerJobID)

	type StatusStruct struct {
		Status common.StatusType `json:"status"`
	}

	tmp, err := fetchSingleRowAsStruct[StatusStruct](dbCon, query, logData, fn)
	if err != nil {
		return -1, err
	}

	if tmp == nil {
		return -1, nil
	}

	return tmp.Status, nil

}

func getRunJobnetDataByInnerJobID(dbCon database.DBConnection, innerJobID uint64) (*common.RunJobnetTable, error) {
	fn := "getJobnetStatusByInnerJobID"
	logData := logger.Logging{}

	tableName := common.Ja2RunJobnetTable
	query := fmt.Sprintf(`
		SELECT *
		FROM %s 
		WHERE inner_job_id = %d
	`, tableName, innerJobID)

	return fetchSingleRowAsStruct[common.RunJobnetTable](dbCon, query, logData, fn)

}

func getEndBeginIconPairs(dbCon database.DBConnection, innerJobnetID uint64) ([]EndBeginIconPair, error) {
	fn := "getEndBeginIconPairs"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
	}

	query := fmt.Sprintf(`
		SELECT
			j_end.inner_job_id      AS end_inner_job_id,
			j_end.inner_jobnet_id,
			j_end.job_type          AS end_job_type,
			j_end.status,
			j_begin.inner_job_id    AS begin_inner_job_id,
			j_begin.job_type        AS begin_job_type
		FROM
			ja_2_run_flow_table f
		JOIN ja_2_run_job_table j_end
			ON j_end.inner_job_id = f.start_inner_job_id
			AND j_end.inner_jobnet_id = %d
			AND j_end.status = %d
		JOIN ja_2_run_job_table j_begin
			ON j_begin.inner_job_id = f.end_inner_job_id
			AND j_begin.inner_jobnet_id = %d
			AND j_begin.status = %d
	`, innerJobnetID, common.StatusEnd, innerJobnetID, common.StatusBegin)
	endBeginIconPairs, err := fetchRowsAsStructs[EndBeginIconPair](dbCon, query, logData, fn)
	if err != nil {
		return nil, err
	}

	uniqueEndBeginIconPairs := []EndBeginIconPair{}

	foundEndJobs := make(map[uint64]bool)
	for _, endBeginPair := range endBeginIconPairs {
		if foundEndJobs[endBeginPair.EndInnerJobID] {
			logger.JaLog("JARECOVERYREEXEC400035", logData, fn, endBeginPair)
			continue
		}
		foundEndJobs[endBeginPair.EndInnerJobID] = true
		uniqueEndBeginIconPairs = append(uniqueEndBeginIconPairs, endBeginPair)
	}

	return uniqueEndBeginIconPairs, nil

}

func getOtherNextIconData(dbCon database.DBConnection, innerJobnetID, innerJobID, beginInnerJobID uint64) (*common.RunJobTable, error) {
	fn := "getOtherNextIconData"
	logData := logger.Logging{
		InnerJobnetID: innerJobnetID,
		InnerJobID:    innerJobID,
	}

	logger.JaLog("JARECOVERYREEXEC400045", logData, fn, innerJobnetID, innerJobID)

	runFlows, err := getRunFlowData(dbCon, innerJobnetID, innerJobID)
	if err != nil {
		return nil, fmt.Errorf("failed to get run flow data for inner_jobnet_id [%d], inner_job_id [%d]: %v", innerJobnetID, innerJobID, err)
	}

	for _, runFlow := range runFlows {
		if runFlow.EndInnerJobID == beginInnerJobID {
			logger.JaLog("JARECOVERYREEXEC400047", logData, fn, beginInnerJobID)
			continue
		}

		// Get the status of the next remaining icon to determine whether IF-icon needs to be re-executed
		runJobData, err := getRunJobData(dbCon, innerJobnetID, runFlow.EndInnerJobID)
		if err != nil {
			return nil, fmt.Errorf("failed to get icon data for inner_jobnet_id [%d], inner_job_id [%d]", innerJobnetID, runFlow.EndInnerJobID)
		}

		logger.JaLog("JARECOVERYREEXEC400046", logData, fn, innerJobnetID, innerJobID, runJobData)

		return runJobData, nil
	}

	return nil, fmt.Errorf("no other next icon is found")
}

func HandleHostLockFlagFiles(dbCon database.DBConnection) error {
	fn := "HandleHostLockFlagFiles"

	hostLockFolderPath := filepath.Join(common.ServerTmpFolderPath, common.FlowManagerFolder, common.FlowManagerSubFolders[4])
	if err := os.MkdirAll(hostLockFolderPath, 0755); err != nil {
		return fmt.Errorf("failed to create host lock folder: %w", err)
	}

	if err := builder.DeleteFilesUnderFolder(hostLockFolderPath, "*"); err != nil {
		return err
	}
	query := fmt.Sprintf("SELECT * FROM %s where lock_host_name <> 'HOST_LOCK_RECORD' ", common.Ja2HostLockTable)
	lockedHostData, err := fetchRowsAsStructs[common.HostLockTable](dbCon, query, logger.Logging{}, fn)
	if err != nil {
		return err
	}
	for _, lockedHost := range lockedHostData {
		if err = builder.CreateUniqueFile(hostLockFolderPath, lockedHost.Hostname); err != nil {
			return err
		}
	}
	return nil
}

func removeAllInDir(dirPath string) error {
	entries, err := os.ReadDir(dirPath)
	if err != nil {
		return fmt.Errorf("failed to read directory %s: %s", dirPath, utils.ErrMsgWithErrno(err))
	}

	for _, entry := range entries {
		path := filepath.Join(dirPath, entry.Name())
		if err := os.RemoveAll(path); err != nil {
			return fmt.Errorf("failed to remove %s: %s", path, utils.ErrMsgWithErrno(err))
		}
	}

	return nil
}

func moveAllFiles(srcDir, destDir string) error {
	fn := "moveAllFiles"

	// Ensure destination directory exists
	if err := os.MkdirAll(destDir, common.FilePermission); err != nil {
		return fmt.Errorf("failed to create destination directory %s: %s", destDir, utils.ErrMsgWithErrno(err))
	}

	entries, err := os.ReadDir(srcDir)
	if err != nil {
		return fmt.Errorf("failed to read source directory %s: %s", srcDir, utils.ErrMsgWithErrno(err))
	}

	for _, entry := range entries {
		srcPath := filepath.Join(srcDir, entry.Name())
		destPath := filepath.Join(destDir, entry.Name())

		if err := os.Rename(srcPath, destPath); err != nil {
			return fmt.Errorf("failed to move %s to %s: %s", srcPath, destPath, utils.ErrMsgWithErrno(err))
		}

		logger.JaLog("JARECOVERYREEXEC400059", logger.Logging{}, fn, srcPath, destPath)
	}

	return nil
}

func removeEventFilesForRanJobnets(dbCon database.DBConnection, eventFiles []common.EventFileNameParts) ([]common.EventFileNameParts, error) {
	fn := "removeEventFilesForRanJobnets"
	logData := logger.Logging{}

	logger.JaLog("JARECOVERYREEXEC400014", logData, fn)

	// For ja_2_ran_jobnet_table, clean up all corresponding event files
	tableName := "ja_2_ran_jobnet_table"
	query := fmt.Sprintf("SELECT * FROM %s", tableName)
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	dbResult, err := dbCon.Select(query)
	if err != nil {
		return eventFiles, fmt.Errorf("failed to select from %s: %v", tableName, err)
	}
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return eventFiles, fmt.Errorf("failed to fetch row from %s: %v", tableName, err)
		}

		logger.JaLog("JARECOVERYREEXEC400015", logData, fn, row)
		var runJobnetData common.RunJobnetTable
		err = utils.MapStringStringToStruct(row, &runJobnetData)
		if err != nil {
			return eventFiles, fmt.Errorf("failed to convert row to 'RunJobnetTable' struct [raw data=%v]: %v", row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, "RunJobnetTable", runJobnetData)

		eventFiles = moveEventFilesByInnerJobnetID(eventFiles, runJobnetData.InnerJobnetID)
	}

	return eventFiles, nil
}

func deleteExtIconTimeout(dbCon database.DBConnection) error {
	fn := "deleteExtIconTimeout"
	logData := logger.Logging{}

	logger.JaLog("JARECOVERYREEXEC400001", logData, fn)

	if err := dbCon.Begin(); err != nil {
		return fmt.Errorf("in %s(), failed to begin transaction: %v", fn, err)
	}

	query := fmt.Sprintf(`
		DELETE FROM %s
		WHERE icon_type = %d
	`, common.Ja2RunTimeOutTable, common.IconTypeExtJob)
	query = strings.Join(strings.Fields(query), " ") // flatten query

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	timeoutDelCount, err := dbCon.Execute(query)
	if err := handleDBExecErr(err, dbCon, fn, query, logData); err != nil {
		return err
	}

	logger.JaLog("JARECOVERYREEXEC400004", logData, fn, timeoutDelCount)
	return nil
}

// helper function to rollback if query execution or commit has failed
// it commits if query execution has succeeded
func handleDBExecErr(err error, dbCon database.DBConnection, fn, query string, logData logger.Logging) error {
	if err != nil {
		errMsg := fmt.Sprintf("in %s(), failed to execute query '%s': %v", fn, query, err)
		if rbErr := dbCon.Rollback(); rbErr != nil {
			return fmt.Errorf("%s. also failed to rollback: %v", errMsg, rbErr)
		}

		logger.JaLog("JARECOVERYREEXEC400003", logData, fn)
		return fmt.Errorf("%s", errMsg)
	}

	if err := dbCon.Commit(); err != nil {
		errMsg := fmt.Sprintf("in %s(), failed to commit query '%s': %v", fn, query, err)
		if rbErr := dbCon.Rollback(); rbErr != nil {
			return fmt.Errorf("%s. also failed to rollback: %v", errMsg, rbErr)
		}

		logger.JaLog("JARECOVERYREEXEC400003", logData, fn)
		return fmt.Errorf("%s", errMsg)
	}

	return nil
}

// generic helper function to fetch single row from the database
func fetchSingleRowAsStruct[T any](
	dbCon database.DBConnection,
	query string,
	logData logger.Logging,
	fn string,
) (*T, error) {

	query = strings.Join(strings.Fields(query), " ") // flatten query

	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	dbResult, err := dbCon.Select(query)
	if err != nil {
		return nil, fmt.Errorf("failed to select query %q: %v", query, err)
	}
	defer dbResult.Free()

	if !dbResult.HasNextRow() {
		logger.JaLog("JARECOVERYREEXEC400030", logData, fn, query)
		return nil, nil
	}

	row, err := dbResult.Fetch()
	if err != nil {
		return nil, fmt.Errorf("failed to fetch row for query %q: %v", query, err)
	}

	logger.JaLog("JARECOVERYREEXEC400043", logData, fn, query, row)

	var data T
	err = utils.MapStringStringToStruct(row, &data)
	if err != nil {
		return nil, fmt.Errorf("failed to convert row to struct (%T) [raw data=%v]: %v", data, row, err)
	}

	logger.JaLog("JARECOVERYREEXEC400016", logData, fn, fmt.Sprintf("%T", data), data)
	return &data, nil
}

// fetchRowsAsStructs executes a query and maps all returned rows into a slice of structs.
func fetchRowsAsStructs[T any](
	dbCon database.DBConnection,
	query string,
	logData logger.Logging,
	fn string,
) ([]T, error) {

	query = strings.Join(strings.Fields(query), " ") // flatten query
	logger.JaLog("JARECOVERYREEXEC400002", logData, fn, query)

	dbResult, err := dbCon.Select(query)
	if err != nil {
		return nil, fmt.Errorf("failed to select query %q: %v", query, err)
	}
	defer dbResult.Free()

	var results []T
	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return nil, fmt.Errorf("failed to fetch row for query %q: %v", query, err)
		}

		logger.JaLog("JARECOVERYREEXEC400043", logData, fn, query, row)

		var data T
		if err := utils.MapStringStringToStruct(row, &data); err != nil {
			return nil, fmt.Errorf("failed to map row to struct (%T) [raw=%v]: %v", data, row, err)
		}

		logger.JaLog("JARECOVERYREEXEC400016", logData, fn, fmt.Sprintf("%T", data), data)

		results = append(results, data)
	}

	return results, nil
}
