/*
** 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 (
	"archive/tar"
	"compress/gzip"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"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"
	wm "jobarranger2/src/libs/golibs/worker_manager"
)

var (
	keepSpanInformation keepSpanInfo
	moveIntervalSeconds = int64(60) // default
	lastIntervalCalcDay = ""
	lastMoveUnix        int64
)

type keepSpanInfo struct {
	jobnetKeepSpan      int
	joblogKeepSpan      int
	sendmessageKeepSpan int
}

type ChildTable struct {
	Name string
	PK   string
}

var childTables = []ChildTable{
	{"ja_2_ran_flow_table", "inner_flow_id"},
	{"ja_2_ran_value_job_table", "seq_no"},
	{"ja_2_ran_value_jobcon_table", "seq_no"},
	{"ja_2_ran_job_table", "inner_jobnet_id"},
}

type Keeper struct {
	MoveRunTables   []string
	MoveRanTables   []string
	DeleteRunTables []string
	DeleteRanTables []string
	TransactionDir  string
}

var keeper = Keeper{
	MoveRunTables:   []string{"ja_2_run_jobnet_table", "ja_2_run_jobnet_summary_table", "ja_2_run_job_table", "ja_2_run_flow_table", "ja_2_run_value_job_table", "ja_2_run_value_jobcon_table", "ja_2_run_job_variable_table", "ja_2_run_jobnet_variable_table", "ja_2_run_log_table"},
	MoveRanTables:   []string{"ja_2_ran_jobnet_table", "ja_2_ran_jobnet_summary_table", "ja_2_ran_job_table", "ja_2_ran_flow_table", "ja_2_ran_value_job_table", "ja_2_ran_value_jobcon_table", "ja_2_ran_job_variable_table", "ja_2_ran_jobnet_variable_table", "ja_2_ran_log_table"},
	DeleteRunTables: []string{"ja_2_run_log_table", "ja_2_run_jobnet_variable_table", "ja_2_run_job_variable_table", "ja_2_run_value_jobcon_table", "ja_2_run_value_job_table", "ja_2_run_flow_table", "ja_2_run_job_table", "ja_2_run_jobnet_summary_table", "ja_2_run_jobnet_table"},
	DeleteRanTables: []string{"ja_2_ran_log_table", "ja_2_ran_jobnet_variable_table", "ja_2_ran_job_variable_table", "ja_2_ran_value_jobcon_table", "ja_2_ran_value_job_table", "ja_2_ran_flow_table", "ja_2_ran_job_table", "ja_2_ran_jobnet_summary_table", "ja_2_ran_jobnet_table"},
}

var errThreadholdReach = errors.New("threadhold has reached")

func ProcessEventData(data common.Data) {
}

func StartDaemonWorkers(data common.Data) {
	wm.StartWorker(func() { handleJobnetErrors(data.DB) }, "handleJobnetErrors", 600, 0, string(common.HousekeeperManagerProcess))
	wm.StartWorker(func() { moveRunToRanTable(data.DB) }, "moveRunToRanTable", server.Options.JaPurgeTimeout, 0, string(common.HousekeeperManagerProcess))
	wm.StartWorker(func() { MainJapurgeLoop(data.DB) }, "MainJapurgeLoop", server.Options.JaPurgeTimeout, 0, string(common.HousekeeperManagerProcess))
	wm.StartWorker(func() { archiveTransactionFiles() }, "archiveTransactionFiles", 3600, 0, string(common.HousekeeperManagerProcess))
}

func handleJobnetErrors(db database.Database) {
	const functionName = "handleJobnetErrors"
	const batchSize = 10

	dbCon, err := db.GetConn()
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200009", logger.Logging{}, functionName, err.Error())
		os.Exit(1)
	}

	defer dbCon.EndSession()

	for {
		workerId := "handleJobnetErrors"
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAHOUSEKEEPER400006", logger.Logging{}, functionName, workerId)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- workerId
		}

		timeout := time.Duration(server.Options.JaChangeStatusTime) * time.Second
		cutoffTime := time.Now().Add(-timeout).Unix()

		query := fmt.Sprintf(`SELECT r.inner_jobnet_main_id, r.jobnet_id
		FROM %s r 
		JOIN %s s ON r.inner_jobnet_id=s.inner_jobnet_id 
		WHERE r.status=%d 
  		AND (
       	(r.end_time > 0 AND r.end_time <= %d)
        OR 
    	(r.end_time = 0 AND s.load_status=2 AND s.update_date <= %d)
      	)
		ORDER BY r.inner_jobnet_main_id ASC 
		LIMIT %d`,
			common.Ja2RunJobnetTable,
			common.Ja2RunJobnetSummaryTable,
			common.StatusRunErr,
			cutoffTime, cutoffTime, batchSize)

		logger.JaLog("JAHOUSEKEEPER400017", logger.Logging{}, functionName)

		dbResult, err := dbCon.Select(query)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, query, err.Error())
			return
		}

		for dbResult.HasNextRow() {
			row, err := dbResult.Fetch()
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
				break
			}

			innerJobnetMainIDStr := row["inner_jobnet_main_id"]
			if innerJobnetMainIDStr == "" {
				logger.JaLog("JAHOUSEKEEPER200004", logger.Logging{}, functionName, innerJobnetMainIDStr)
				continue
			}

			jobnetIDStr := row["jobnet_id"]
			if jobnetIDStr == "" {
				logger.JaLog("JAHOUSEKEEPER200004", logger.Logging{}, functionName, jobnetIDStr)
				continue
			}

			// Convert string IDs to int
			InnerJobnetID, err := strconv.ParseUint(innerJobnetMainIDStr, 10, 64)
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200029", logger.Logging{}, functionName, innerJobnetMainIDStr, err.Error())
			}

			var data common.EventData
			data.Event.Name = common.EventJobnetStop
			data.Event.UniqueKey = common.GetUniqueKey(common.HousekeeperManagerProcess)
			data.NextProcess.Data = common.JobnetRunData{
				InnerJobnetId: InnerJobnetID,
				JobnetID:      jobnetIDStr,
			}
			data.NextProcess.Name = common.JobnetManagerProcess

			if err := sendEventToJobnetManager(data); err != nil {
				logger.JaLog("JAHOUSEKEEPER200008", logger.Logging{}, functionName, err.Error())
			}

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

func moveRunToRanTable(db database.Database) {
	const functionName = "moveRunToRanTable"
	const batchSize = 5 // batch size for moving jobnets

	seenMainIDs := make(map[string]struct{})
	seenJobnetIDs := make(map[string]struct{})

	dbCon, err := db.GetConn()
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200009", logger.Logging{}, functionName, err.Error())
		os.Exit(1)
	}
	defer dbCon.EndSession()

	for {
		workerId := "moveRunToRanTable"
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAHOUSEKEEPER400006", logger.Logging{}, functionName, workerId)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- workerId
		}

		// ===== DAILY INTERVAL ADJUSTMENT =====
		now := time.Now()
		today := now.Format("20060102")
		if now.Hour() == 12 && today != lastIntervalCalcDay {
			moveIntervalSeconds = calculateMoveInterval(dbCon)
			lastIntervalCalcDay = today
			logger.JaLog("JAHOUSEKEEPER400026", logger.Logging{}, functionName, moveIntervalSeconds)
		}

		moveDelay := time.Duration(server.Options.RunRecordMoveTime) * time.Second
		moveTime := time.Now().Add(-moveDelay).Unix()

		query := fmt.Sprintf(`
			SELECT 
				r.inner_jobnet_main_id,
				r.inner_jobnet_id,
				EXISTS (
					SELECT 1 
					FROM %s t
					WHERE t.inner_jobnet_id = r.inner_jobnet_id
				) AS already_exists
			FROM %s r
			WHERE r.status IN (%d, %d)
			AND r.end_time <= %d
			ORDER BY r.inner_jobnet_main_id ASC
			LIMIT %d
		`,
			common.Ja2RanJobnetTable,
			common.Ja2RunJobnetTable,
			common.StatusEnd,
			common.StatusEndErr,
			moveTime,
			batchSize,
		)

		logger.JaLog("JAHOUSEKEEPER400018", logger.Logging{}, functionName)

		result, err := dbCon.Select(query)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, query, err.Error())
			return
		}

		var idsToDelete []string
		var idsToMove []string

		for result.HasNextRow() {
			row, err := result.Fetch()
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
				break
			}

			mainID, ok := row["inner_jobnet_main_id"]
			if !ok {
				continue
			}

			// Run Count files cleanup
			if _, already := seenMainIDs[mainID]; !already {
				seenMainIDs[mainID] = struct{}{}
				mainIDInt, err := strconv.ParseUint(mainID, 10, 64)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200029", logger.Logging{}, functionName, mainID, err.Error())
					continue
				}
				event.DeleteRunCountFile(
					filepath.Join(server.Options.TmpDir, common.IconExecManagerFolder, common.RunCountFolder),
					mainIDInt,
				)
			}

			jobnetID := row["inner_jobnet_id"]

			alreadyMoved := false
			if val, ok := row["already_exists"]; ok {
				if val == "1" || val == "t" || val == "true" || val == "TRUE" {
					alreadyMoved = true
				}
			}

			if !alreadyMoved {
				idsToMove = append(idsToMove, jobnetID)
			}

			idsToDelete = append(idsToDelete, jobnetID)

			// tran & sock files cleanup
			if _, already := seenJobnetIDs[jobnetID]; !already {
				seenJobnetIDs[jobnetID] = struct{}{}
				cleanupFilesForAllManagers(jobnetID)
			}
		}
		result.Free()

		// Move from run to ran tables
		if len(idsToMove) != 0 {
			toMoveIdList := strings.Join(idsToMove, ",")

			// Begin transaction
			if err := dbCon.Begin(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
				return
			}

			// Batch insert into ran tables
			for i := range keeper.MoveRunTables {
				runTable := keeper.MoveRunTables[i]
				ranTable := keeper.MoveRanTables[i]

				insertQuery := fmt.Sprintf(`
				INSERT INTO %s SELECT * FROM %s
				WHERE inner_jobnet_id IN (%s)
			`, ranTable, runTable, toMoveIdList)

				if _, err := dbCon.Execute(insertQuery); err != nil {
					logger.JaLog("JAHOUSEKEEPER200013", logger.Logging{}, functionName, ranTable, insertQuery, err.Error())
					dbCon.Rollback()
					return
				}
				logger.JaLog("JAHOUSEKEEPER400024", logger.Logging{}, functionName, ranTable, idsToMove)
			}

			// Commit transaction
			if err := dbCon.Commit(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200015", logger.Logging{}, functionName, err.Error())
				dbCon.Rollback()
				return
			}
		}

		// Delete from run tables
		if len(idsToDelete) != 0 {
			var deleteQuery string
			toDeleteIdList := strings.Join(idsToDelete, ",")

			for i := range keeper.DeleteRunTables {
				runTable := keeper.DeleteRunTables[i]

				for {
					// Prepare delete query with limit
					if server.Options.DBType == database.PostgresDBType {
						deleteQuery = fmt.Sprintf(`
							WITH batch AS (
								SELECT ctid
								FROM %s
								WHERE inner_jobnet_id IN (%s)
								LIMIT %d
							)
							DELETE FROM %s
							WHERE ctid IN (SELECT ctid FROM batch)
						`, runTable, toDeleteIdList, server.Options.JaPurgeLimit, runTable)

					} else {
						deleteQuery = fmt.Sprintf(`
							DELETE FROM %s
							WHERE inner_jobnet_id IN (%s)
							LIMIT %d
						`, runTable, toDeleteIdList, server.Options.JaPurgeLimit)
					}

					// Begin transaction
					if err := dbCon.Begin(); err != nil {
						logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
						return
					}

					// Execute delete query
					affectedRow, err := dbCon.Execute(deleteQuery)
					if err != nil {
						logger.JaLog("JAHOUSEKEEPER200014", logger.Logging{}, functionName, runTable, deleteQuery, err.Error())
						_ = dbCon.Rollback()
						return
					}

					// Commit transaction
					if err := dbCon.Commit(); err != nil {
						logger.JaLog("JAHOUSEKEEPER200015", logger.Logging{}, functionName, err.Error())
						_ = dbCon.Rollback()
						return
					}

					// Check remaining records. Yes -> continue to delete, No -> move to next table
					if affectedRow == 0 {
						break
					}
				}

				logger.JaLog("JAHOUSEKEEPER400025", logger.Logging{}, functionName, runTable, toDeleteIdList)
			}
		}

		// set move time value to lastMoveUnix variable
		atomic.StoreInt64(&lastMoveUnix, time.Now().Unix())
		seenMainIDs = make(map[string]struct{})
		seenJobnetIDs = make(map[string]struct{})

		// Purge Old Job file Under TrapperManager 's job folder
		err = purgeTrapperOldJobFile(time.Duration(server.Options.JaPurgeJobFileInterval) * 24 * time.Hour)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER400025", logger.Logging{}, functionName, filepath.Join(common.TrapperManagerFolder, "job"), err.Error())
			return
		}

		//SLEEP — now dynamic
		time.Sleep(time.Duration(moveIntervalSeconds) * time.Second)
	}
}

func calculateMoveInterval(dbCon database.DBConnection) int64 {
	const functionName = "calculateMoveInterval"

	// If no move has happened yet, fall back
	if lastMoveUnix == 0 {
		return 60
	}

	// Do not adjust unless 24 hours have passed since last successful move
	last := atomic.LoadInt64(&lastMoveUnix)
	if time.Now().Unix() < last+24*60*60 {
		return moveIntervalSeconds
	}

	// Cutoff time used to count jobnets overdue since last successful move
	cutoff := last + 24*60*60

	query := fmt.Sprintf(`
		SELECT COUNT(*) AS cnt
		FROM %s
		WHERE end_time <= %d
		  AND status IN (%d, %d)
	`,
		common.Ja2RunJobnetTable,
		cutoff,
		common.StatusEnd,
		common.StatusEndErr,
	)

	res, err := dbCon.Select(query)
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, query, err.Error())
		return 60
	}
	defer res.Free()

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

	count, err := strconv.Atoi(row["cnt"])
	if err != nil || count <= 0 {
		return 60
	}

	// 23 hours divided by number of overdue jobnets
	interval := int64((23 * 60 * 60) / count)
	if interval < 60 {
		return interval
	}
	return 60
}

func MainJapurgeLoop(db database.Database) {

	const functionName = "MainJapurgeLoop"
	dbCon, err := db.GetConn()
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200009", logger.Logging{}, functionName, err.Error())
		os.Exit(1)
	}

	defer dbCon.EndSession()

	for {
		workerId := "MainJapurgeLoop"
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAHOUSEKEEPER400006", logger.Logging{}, functionName, workerId)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- workerId
		}

		// Get Keep Span Data from ja_2_parameter_table
		getKeepSpan(dbCon, &keepSpanInformation)

		if !isJapurgeTime(server.Options.JaPurgeTime) {
			time.Sleep(30 * time.Second)
			continue
		}

		logger.JaLog("JAHOUSEKEEPER400022", logger.Logging{}, functionName, server.Options.JaPurgeTime)

		jobnetPurgeDate := getPurgeDate(keepSpanInformation.jobnetKeepSpan)
		joblogPurgeDate := getPurgeDate(keepSpanInformation.joblogKeepSpan)
		sendmessagePurgeDate := getPurgeDate(keepSpanInformation.sendmessageKeepSpan)

		start := time.Now()
		if err := JobnetPurge(dbCon, jobnetPurgeDate, server.Options.JaPurgeLimit, server.Options.JaPurgeTime); err != nil {
			logger.JaLog("JAHOUSEKEEPER200016", logger.Logging{}, functionName, err.Error())
		}
		logger.JaLog("JAHOUSEKEEPER400011", logger.Logging{}, functionName, time.Since(start).Seconds())

		if err := JoblogPurge(dbCon, joblogPurgeDate, server.Options.JaPurgeLimit, server.Options.JaPurgeTime); err != nil {
			logger.JaLog("JAHOUSEKEEPER200017", logger.Logging{}, functionName, err.Error())
		}
		logger.JaLog("JAHOUSEKEEPER400012", logger.Logging{}, functionName, time.Since(start).Seconds())

		if err := SendMessagePurge(dbCon, sendmessagePurgeDate, server.Options.JaPurgeLimit, server.Options.JaPurgeTime); err != nil {
			logger.JaLog("JAHOUSEKEEPER200025", logger.Logging{}, functionName, err.Error())
		}
		logger.JaLog("JAHOUSEKEEPER400020", logger.Logging{}, functionName, time.Since(start).Seconds())

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

func archiveTransactionFiles() {
	const functionName = "archiveTransactionFiles"
	managers := []string{
		"dbsyncermanager", "flowmanager", "iconexecmanager",
		"iconresultmanager", "jobnetmanager", "notificationmanager",
	}

	for {
		workerId := "archiveTransactionFiles"
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JAHOUSEKEEPER400006", logger.Logging{}, functionName, workerId)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- workerId
		}

		for _, mgr := range managers {
			for _, sub := range []string{"end", "error"} {
				folder := filepath.Join(server.Options.TmpDir, mgr, sub)
				if _, err := os.Stat(folder); os.IsNotExist(err) {
					continue
				}

				var fileCount int
				var totalSize int64
				grouped := make(map[string][]string)

				err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error {
					if err != nil {
						if errors.Is(err, fs.ErrNotExist) {
							// Path no longer exists, skip
							return nil
						}
						// Any other error → stop walking
						return fmt.Errorf("failed to read %s: %w", path, err)
					}

					// Skip directories
					if d.IsDir() {
						return nil
					}

					name := d.Name()
					if strings.HasSuffix(name, ".tar.gz") {
						return nil
					}

					info, err := d.Info()
					if err != nil {
						logger.JaLog("JAHOUSEKEEPER000004", logger.Logging{}, functionName, path, err.Error())
						return nil
					}

					fileCount++
					totalSize += info.Size()

					parts := strings.SplitN(name, "_", 4)
					if len(parts) < 2 {
						return nil
					}
					key := parts[0] + "_" + parts[1]
					grouped[key] = append(grouped[key], path)

					if fileCount >= 1000 || totalSize >= 5*1024*1024 {
						return errThreadholdReach
					}

					return nil
				})

				if err != nil && !errors.Is(err, errThreadholdReach) {
					logger.JaLog("JAHOUSEKEEPER200026", logger.Logging{}, functionName, folder, err.Error())
					continue
				}
				// Trigger archive only if threshold reached
				if fileCount < 1000 && totalSize < 5*1024*1024 {
					logger.JaLog("JAHOUSEKEEPER400021", logger.Logging{}, functionName, mgr, sub, fileCount, float64(totalSize)/(1024*1024))
					continue
				}

				logger.JaLog("JAHOUSEKEEPER400022", logger.Logging{}, functionName, mgr, sub, fileCount, float64(totalSize)/(1024*1024))

				var archivedFiles int
				const maxFilesPerManager = 500

				for id, files := range grouped {
					if archivedFiles >= maxFilesPerManager {
						break
					}

					if len(files) == 0 {
						continue
					}

					for i := 0; i < len(files) && archivedFiles < maxFilesPerManager; i += 100 {
						end := i + 100
						if end > len(files) {
							end = len(files)
						}
						// Limit batch to not exceed total manager cap
						if archivedFiles+(end-i) > maxFilesPerManager {
							end = i + (maxFilesPerManager - archivedFiles)
						}

						batch := files[i:end]
						timestamp := time.Now().Format("20060102150405")
						newArchive := filepath.Join(folder, id+"_"+timestamp+".tar.gz")

						if err := createTarGz(newArchive, batch); err != nil {
							logger.JaLog("JAHOUSEKEEPER000002", logger.Logging{}, functionName, newArchive, err.Error())
							continue
						}

						for _, f := range batch {
							_ = os.Remove(f)
						}

						logger.JaLog("JAHOUSEKEEPER400023", logger.Logging{}, functionName, len(batch), newArchive)
						archivedFiles += len(batch)

						if archivedFiles < maxFilesPerManager {
							time.Sleep(2 * time.Second)
						}
					}
				}

				logger.JaLog("JAHOUSEKEEPER400024", logger.Logging{}, functionName, mgr, sub, archivedFiles)
			}
		}

		// Wait until next archive interval
		time.Sleep(time.Duration(server.Options.ArchiveInterval) * time.Second)
	}
}

// createTarGz creates a tar.gz including files (can include previous archive)
func createTarGz(tarGzPath string, files []string) error {
	twFile, err := os.Create(tarGzPath)
	if err != nil {
		return err
	}
	defer twFile.Close()

	gw := gzip.NewWriter(twFile)
	defer gw.Close()

	tw := tar.NewWriter(gw)
	defer tw.Close()

	for _, f := range files {
		if err := addFileToTar(tw, f); err != nil {
			return err
		}
	}
	return nil
}

// addFileToTar embeds a file into a tar (even another tar.gz)
func addFileToTar(tw *tar.Writer, filePath string) error {
	src, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer src.Close()

	info, err := src.Stat()
	if err != nil {
		return err
	}

	hdr, err := tar.FileInfoHeader(info, "")
	if err != nil {
		return err
	}
	hdr.Name = filepath.Base(filePath)

	if err := tw.WriteHeader(hdr); err != nil {
		return err
	}

	if _, err := io.Copy(tw, src); err != nil {
		return err
	}

	return nil
}

func JobnetPurge(dbCon database.DBConnection, purgeDate int64, japurgeLimit int, japurgeTime string) error {
	const functionName = "JobnetPurge"
	logger.JaLog("JAHOUSEKEEPER400009", logger.Logging{}, functionName, purgeDate, japurgeLimit, japurgeTime)

	query := fmt.Sprintf(`
		SELECT inner_jobnet_id, jobnet_id, update_date, status, inner_jobnet_main_id
		FROM %s WHERE inner_jobnet_main_id IN (
			SELECT inner_jobnet_id
			FROM %s WHERE end_time <= %d)`,
		common.Ja2RanJobnetTable, common.Ja2RanJobnetSummaryTable, purgeDate)

	result, err := dbCon.Select(query)
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, query, err.Error())
		return err
	}

	defer result.Free()

	for result.HasNextRow() {

		row, err := result.Fetch()
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
			continue
		}

		innerID := row["inner_jobnet_id"]
		mainID := row["inner_jobnet_main_id"]

		// before Begin() — otherwise duplicate transaction error happens
		if err := dbCon.Begin(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
			return err
		}

		// If innerID == mainID: update delete_flag on summary table
		if innerID == mainID {

			checkQuery := fmt.Sprintf(`
				SELECT COUNT(*) as count 
				FROM %s 
				WHERE inner_jobnet_id = %s AND delete_flag = 0 LIMIT 1`,
				common.Ja2RanJobnetSummaryTable, mainID)

			checkRes, err := dbCon.Select(checkQuery)
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, checkQuery, err.Error())
				_ = dbCon.Rollback()
				continue
			}

			chkRow, err := checkRes.Fetch()
			checkRes.Free()
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
				_ = dbCon.Rollback()
				continue
			}

			if chkRow["count"] == "1" {
				upt := fmt.Sprintf(`UPDATE %s SET delete_flag = 1 WHERE inner_jobnet_id = %s`,
					common.Ja2RanJobnetSummaryTable, mainID)

				if _, err := dbCon.Execute(upt); err != nil {
					logger.JaLog("JAHOUSEKEEPER200018", logger.Logging{}, functionName,
						mainID, innerID, row["update_date"], upt, err.Error())

					_ = dbCon.Rollback()
					continue
				}
				logger.JaLog("JAHOUSEKEEPER400019", logger.Logging{}, functionName, innerID)
			}
		}

		// Purge each child table repeatedly until empty
		for _, tbl := range childTables {

			var deleteQuery, countQuery string
			var deletedRows int

			if server.Options.DBType == database.PostgresDBType {
				deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE ctid IN 
					(SELECT ctid FROM %s WHERE inner_jobnet_id = %s LIMIT %d)`,
					tbl.Name, tbl.Name, innerID, japurgeLimit)

				countQuery = fmt.Sprintf(`SELECT COUNT(*) as count FROM %s WHERE inner_jobnet_id = %s`,
					tbl.Name, innerID)
			} else {
				deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE inner_jobnet_id = %s LIMIT %d`,
					tbl.Name, innerID, japurgeLimit)

				countQuery = fmt.Sprintf(`SELECT COUNT(*) as count FROM %s WHERE inner_jobnet_id = %s LIMIT 1`,
					tbl.Name, innerID)
			}

			for {
				// DELETE
				result, err := dbCon.Execute(deleteQuery)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200010", logger.Logging{}, functionName, tbl.Name, deleteQuery, err.Error())
					time.Sleep(1 * time.Second)
					continue
				}

				deletedRows += result
				// COUNT remaining rows
				cntRes, err := dbCon.Select(countQuery)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, countQuery, err.Error())
					continue
				}

				cntRow, err := cntRes.Fetch()
				cntRes.Free()

				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
					continue
				}

				// If empty → stop deleting
				if cntRow["count"] == "0" {
					break
				}
			}

			logger.JaLog("JAHOUSEKEEPER400008", logger.Logging{}, functionName, tbl.Name, innerID, deletedRows)
		}

		// Now safe to delete the jobnet row after all tables empty
		if innerID == mainID {

			chkQuery := fmt.Sprintf(`
				SELECT COUNT(*) as count 
				FROM %s 
				WHERE inner_jobnet_main_id = %s LIMIT 2`,
				common.Ja2RanJobnetTable, mainID)

			chkRes, err := dbCon.Select(chkQuery)
			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, chkQuery, err.Error())
				_ = dbCon.Rollback()
				continue
			}

			chkRow, err := chkRes.Fetch()
			chkRes.Free()

			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
				_ = dbCon.Rollback()
				continue
			}

			cnt, _ := strconv.Atoi(chkRow["count"])
			if cnt > 1 {
				_ = dbCon.Rollback()
				logger.JaLog("JAHOUSEKEEPER000001", logger.Logging{}, functionName, mainID)
				continue
			}
		}

		// DELETE main jobnet row
		delQuery := fmt.Sprintf(`DELETE FROM %s WHERE inner_jobnet_id = %s`,
			common.Ja2RanJobnetTable, innerID)

		result, err := dbCon.Execute(delQuery)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200020",
				logger.Logging{}, functionName, common.Ja2RanJobnetTable, delQuery, err.Error())

			_ = dbCon.Rollback()
			break
		}

		// Commit OK
		if err := dbCon.Commit(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200015", logger.Logging{}, functionName, err.Error())
			_ = dbCon.Rollback()
		}

		logger.JaLog("JAHOUSEKEEPER400013", logger.Logging{}, functionName, result, innerID)
	}

	return nil
}

// JoblogPurge purges old job logs before the given cutoff date string (format: YYYYMMDD)
func JoblogPurge(dbCon database.DBConnection, purgeDate int64, japurgeLimit int, japurgeTime string) error {
	const functionName = "JoblogPurge"
	var deleteQuery string

	// Convert purgeDate (seconds) → nanoseconds
	// purgeDateNano := purgeDate * 1_000_000_000
	logger.JaLog("JAHOUSEKEEPER400009", logger.Logging{}, functionName, purgeDate, japurgeLimit, japurgeTime)

	// Step 1: Select inner_jobnet_main_id values to purge
	query := fmt.Sprintf(`
	SELECT DISTINCT inner_jobnet_main_id
	FROM %s 
	WHERE log_date <= %d`, common.Ja2RanLogTable, purgeDate)

	rows, err := dbCon.Select(query)
	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200002", logger.Logging{}, functionName, query, err.Error())
		return err
	}
	defer rows.Free()

	for rows.HasNextRow() {
		row, err := rows.Fetch()
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200003", logger.Logging{}, functionName, err.Error())
			continue
		}

		innerJobnetMainID := row["inner_jobnet_main_id"]

		// Check if purge time is valid
		if !isJapurgeTime(japurgeTime) {
			time.Sleep(10 * time.Second)
			continue
		}

		logger.JaLog("JAHOUSEKEEPER400004", logger.Logging{}, functionName, innerJobnetMainID)

		if err := dbCon.Begin(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
			return err
		}

		dbType := server.Options.DBType
		switch dbType {
		case database.MysqlDBType, database.MariaDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE inner_jobnet_main_id = %s LIMIT %d`, common.Ja2RanLogTable, innerJobnetMainID, japurgeLimit)
		case database.PostgresDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE ctid IN (SELECT ctid FROM %s WHERE inner_jobnet_main_id = %s LIMIT %d)`, common.Ja2RanLogTable, common.Ja2RanLogTable, innerJobnetMainID, japurgeLimit)
		}

		result, err := dbCon.Execute(deleteQuery)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200020", logger.Logging{}, functionName, common.Ja2RanLogTable, deleteQuery, err.Error())
			if err := dbCon.Rollback(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200019", logger.Logging{}, functionName, err.Error())
				return err
			}
			continue
		}

		// Commit
		if err := dbCon.Commit(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200015", logger.Logging{}, functionName, err.Error())
			if err := dbCon.Rollback(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200019", logger.Logging{}, functionName, err.Error())
				return err
			}
			continue
		}

		logger.JaLog("JAHOUSEKEEPER400014", logger.Logging{}, functionName, result, innerJobnetMainID)
		time.Sleep(1 * time.Second)
	}

	return nil
}

func SendMessagePurge(dbCon database.DBConnection, purgeDate int64, japurgeLimit int, japurgeTime string) error {
	const functionName = "SendMessagePurge"
	var deleteQuery string

	for {
		// Check if purge time is valid
		if !isJapurgeTime(japurgeTime) {
			time.Sleep(10 * time.Second)
			break
		}

		if err := dbCon.Begin(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
			return err
		}

		dbType := server.Options.DBType
		switch dbType {
		case database.MysqlDBType, database.MariaDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE send_status = %d AND send_date <= %d LIMIT %d`, common.Ja2SendMessageTable, common.JA_SNT_SEND_STATUS_END, purgeDate, japurgeLimit)
		case database.PostgresDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE ctid IN (SELECT ctid FROM %s WHERE send_status = %d AND send_date <= %d LIMIT %d)`, common.Ja2SendMessageTable, common.Ja2SendMessageTable, common.JA_SNT_SEND_STATUS_END, purgeDate, japurgeLimit)
		}

		resultEnd, err := dbCon.Execute(deleteQuery)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200020", logger.Logging{}, functionName, common.Ja2SendMessageTable, deleteQuery, err.Error())
			if err := dbCon.Rollback(); err != nil {
				return err
			}
		}

		if err := dbCon.Commit(); err != nil {
			if err := dbCon.Rollback(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200019", logger.Logging{}, functionName, err.Error())
				return err
			}
		}
		logger.JaLog("JAHOUSEKEEPER400010", logger.Logging{}, functionName, resultEnd, common.JA_SNT_SEND_STATUS_END, purgeDate)

		if err = dbCon.Begin(); err != nil {
			logger.JaLog("JAHOUSEKEEPER200012", logger.Logging{}, functionName, err.Error())
			return err
		}

		switch dbType {
		case database.MysqlDBType, database.MariaDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE send_status = %d AND send_error_date <= %d LIMIT %d`, common.Ja2SendMessageTable, common.JA_SNT_SEND_STATUS_ERROR, purgeDate, japurgeLimit)
		case database.PostgresDBType:
			deleteQuery = fmt.Sprintf(`DELETE FROM %s WHERE ctid IN (SELECT ctid FROM %s WHERE send_status = %d AND send_error_date <= %d LIMIT %d)`, common.Ja2SendMessageTable, common.Ja2SendMessageTable, common.JA_SNT_SEND_STATUS_ERROR, purgeDate, japurgeLimit)
		}

		resultError, err := dbCon.Execute(deleteQuery)
		if err != nil {
			logger.JaLog("JAHOUSEKEEPER200020", logger.Logging{}, functionName, common.Ja2SendMessageTable, deleteQuery, err.Error())
			if err := dbCon.Rollback(); err != nil {
				return err
			}
		}

		if err := dbCon.Commit(); err != nil {
			if err := dbCon.Rollback(); err != nil {
				logger.JaLog("JAHOUSEKEEPER200019", logger.Logging{}, functionName, err.Error())
				return err
			}
		}
		logger.JaLog("JAHOUSEKEEPER400010", logger.Logging{}, functionName, resultError, common.JA_SNT_SEND_STATUS_ERROR, purgeDate)

		if resultEnd+resultError <= 0 {
			break
		}

		time.Sleep(1 * time.Second)
	}

	return nil
}

func getPurgeDate(span int) int64 {
	// Get current time and subtract the span (in minutes)
	purgeTime := time.Now().Add(-time.Duration(span) * time.Minute)

	// Return Unix timestamp (seconds since epoch)
	return purgeTime.Unix()
}

// load Job_Net_Keep_Span, Job_Log_Keep_Span and Send_Message_Keep_Span configuration info from database
func getKeepSpan(dbCon database.DBConnection, keepSpanInformation *keepSpanInfo) error {

	keepSpanMap := map[int]string{
		1: "JOBNET_KEEP_SPAN",
		2: "JOBLOG_KEEP_SPAN",
		3: "SNDMSG_KEEP_SPAN",
	}

	for i := 1; i <= 3; i++ {
		keepSpanName := keepSpanMap[i]
		query := fmt.Sprintf("SELECT value FROM %s WHERE parameter_name = '%s'", common.Ja2ParameterTable, keepSpanName)
		dbResult, err := dbCon.Select(query)
		if err != nil {
			return err
		}
		if dbResult.HasNextRow() {
			row, err := dbResult.Fetch()
			if err != nil {
				dbResult.Free()
				return err
			}

			val := row["value"]
			switch i {
			case 1:
				if v, err := strconv.Atoi(val); err == nil {
					keepSpanInformation.jobnetKeepSpan = v
				}
			case 2:
				if v, err := strconv.Atoi(val); err == nil {
					keepSpanInformation.joblogKeepSpan = v
				}
			case 3:
				if v, err := strconv.Atoi(val); err == nil {
					keepSpanInformation.sendmessageKeepSpan = v
				}
			}
		}
		dbResult.Free()
	}
	return nil
}

func cleanupFilesForAllManagers(jobnetID string) {
	const functionName = "cleanupFilesForAllManagers"

	// Map each manager name to its subfolder slice from common.go
	managerSubFolders := map[string][]string{
		"jobnetmanager":       common.JobnetManagerSubFolders,
		"dbsyncermanager":     common.DBSyncerManagerSubFolders,
		"flowmanager":         common.FlowManagerSubFolders,
		"iconexecmanager":     common.IconExecManagerSubFolders,
		"iconresultmanager":   common.IconResultManagerSubFolders,
		"notificationmanager": common.NotificationManagerSubFolders,
	}

	for manager, subFolders := range managerSubFolders {
		for _, sub := range subFolders {
			// Only target end and error folders
			if sub != "end" && sub != "error" && sub != "pending" {
				continue
			}

			transactionDir := filepath.Join(server.Options.TmpDir, manager, sub)
			err := filepath.WalkDir(transactionDir, func(path string, file fs.DirEntry, err error) error {
				if err != nil {
					if errors.Is(err, fs.ErrNotExist) {
						// Path no longer exists, skip
						return nil
					}
					// Any other error → stop walking
					return fmt.Errorf("failed to read %s: %w", path, err)
				}

				// Skip directories
				if file.IsDir() {
					return nil
				}

				if strings.HasPrefix(file.Name(), jobnetID) {
					if err := os.Remove(path); err == nil {
						logger.JaLog("JAHOUSEKEEPER400015", logger.Logging{}, functionName, path)
					} else {
						logger.JaLog("JAHOUSEKEEPER000003", logger.Logging{}, functionName, path, err.Error())
					}
				}

				return nil
			})

			if err != nil {
				logger.JaLog("JAHOUSEKEEPER200021", logger.Logging{}, functionName, manager, sub, err.Error())
				continue
			}

		}
	}

	// One-time socket cleanup
	cacheFileDir := filepath.Join(server.Options.TmpDir, "iconexecmanager", "client_data")
	err := filepath.WalkDir(cacheFileDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			if errors.Is(err, fs.ErrNotExist) {
				// Path no longer exists, skip
				return nil
			}
			// Any other error → stop walking
			return fmt.Errorf("failed to read %s: %w", path, err)
		}

		// Skip directories
		if d.IsDir() {
			return nil
		}

		name := d.Name()
		if strings.HasPrefix(name, jobnetID) {
			// Disconnect and close ssh client
			if strings.HasSuffix(name, ".sock") {
				fullPath := filepath.Join(cacheFileDir, name)

				conn, err := net.Dial("unix", fullPath)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200024", logger.Logging{}, functionName, path, err.Error())
					return nil
				}
				defer conn.Close()

				closeCmd := `{"command":"close"}`
				data := []byte(closeCmd)

				// Write data length
				length := uint32(len(data))
				err = binary.Write(conn, binary.BigEndian, length)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200024", logger.Logging{}, functionName, path, err.Error())
					return nil
				}

				// Send disconnect request to ssh client
				_, err = conn.Write(data)
				if err != nil {
					logger.JaLog("JAHOUSEKEEPER200024", logger.Logging{}, functionName, path, err.Error())
					return nil
				} else {
					logger.JaLog("JAHOUSEKEEPER400016", logger.Logging{}, functionName, path)
				}
			}

			// Remove .ret file
			if strings.HasSuffix(name, ".ret") {
				if err := os.Remove(path); err == nil {
					logger.JaLog("JAHOUSEKEEPER400027", logger.Logging{}, functionName, path)
				} else {
					logger.JaLog("JAHOUSEKEEPER200030", logger.Logging{}, functionName, path, err.Error())
				}
			}

			// Remove .status file
			if strings.HasSuffix(name, ".status") {
				if err := os.Remove(path); err == nil {
					logger.JaLog("JAHOUSEKEEPER400028", logger.Logging{}, functionName, path)
				} else {
					logger.JaLog("JAHOUSEKEEPER200023", logger.Logging{}, functionName, path, err.Error())
				}
			}

		}

		return nil
	})

	if err != nil {
		logger.JaLog("JAHOUSEKEEPER200022", logger.Logging{}, functionName, err.Error())
	}
}

func sendEventToJobnetManager(data common.EventData) error {

	info := data.NextProcess.Data.(common.JobnetRunData)

	innerJobnetID := info.InnerJobnetId
	innerJobID := info.InnerJobId
	jobnetID := info.JobnetID

	if err := event.CreateNextEvent(data, innerJobnetID, jobnetID, innerJobID); err != nil {
		return fmt.Errorf("json creation failed or send uds failed: %v", err)
	}

	return nil
}

// isJapurgeTime checks if the current time is within the configured purge window.
// If the time string is invalid or set to "00:00-00:00", it returns true to allow deletion anytime.
func isJapurgeTime(timeStr string) bool {
	const functionName = "isJapurgeTime"

	if !isValidTimeFormat(timeStr) {
		logger.JaLog("JAHOUSEKEEPER300002", logger.Logging{}, functionName, timeStr)
		return true
	}

	parts := strings.Split(timeStr, "-")
	if len(parts) != 2 {
		logger.JaLog("JAHOUSEKEEPER300002", logger.Logging{}, functionName, timeStr)
		return true
	}

	startTime, err1 := parseTimeToSeconds(parts[0])
	endTime, err2 := parseTimeToSeconds(parts[1])
	if err1 != nil || err2 != nil {
		logger.JaLog("JAHOUSEKEEPER300003", logger.Logging{}, functionName, timeStr)
		return true
	}

	if startTime == 0 && endTime == 0 {
		return true
	}

	return isTimeInRange(startTime, endTime)
}

// isValidTimeFormat returns true if the input matches "HH:MM-HH:MM".
func isValidTimeFormat(s string) bool {
	if len(s) != 11 {
		return false
	}
	return strings.Count(s, ":") == 2 && strings.Count(s, "-") == 1
}

// parseTimeToSeconds parses "HH:MM" to seconds since midnight.
func parseTimeToSeconds(hm string) (int, error) {
	parts := strings.Split(hm, ":")
	if len(parts) != 2 {
		return 0, fmt.Errorf("invalid time format")
	}
	hour, err1 := strconv.Atoi(parts[0])
	minute, err2 := strconv.Atoi(parts[1])
	if err1 != nil || err2 != nil || hour < 0 || hour >= 24 || minute < 0 || minute >= 60 {
		return 0, fmt.Errorf("invalid time values")
	}
	return (hour*60 + minute) * 60, nil
}

// isTimeInRange checks if the current time in seconds falls between start and end.
func isTimeInRange(start, end int) bool {
	now := time.Now()
	secondsNow := now.Hour()*3600 + now.Minute()*60 + now.Second()

	if start <= end {
		return secondsNow >= start && secondsNow <= end
	}
	// Time window crosses midnight
	return secondsNow >= start || secondsNow <= end
}

func purgeTrapperOldJobFile(maxAge time.Duration) error {
	var err error

	for _, sub := range common.TrapperManagerSubFolders {
		transactionDir := filepath.Join(
			server.Options.TmpDir,
			common.TrapperManagerFolder,
			sub,
		)
		entries, err := os.ReadDir(transactionDir)
		if err != nil {
			return err
		}

		now := time.Now()

		for _, e := range entries {
			if e.IsDir() {
				continue
			}

			path := filepath.Join(transactionDir, e.Name())

			info, err := e.Info()
			if err != nil {
				continue
			}

			age := now.Sub(info.ModTime())

			if age > maxAge {
				err = os.Remove(path)
				if err != nil {
					return err
				}
			}
		}
	}
	return err
}
