/*
** 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"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/agent"
	"jobarranger2/src/libs/golibs/event"
	"jobarranger2/src/libs/golibs/logger/logger"
	"jobarranger2/src/libs/golibs/utils"
	wm "jobarranger2/src/libs/golibs/worker_manager"
)

func housekeeper() {
	fn := "housekeeper"

	sleepTime := 10 // Second
	doneCleanup := false
	configCleanupRunTime := agent.Options.JaCleanupRunTime
	configCleanupTime := agent.Options.JaCleanupTime

	logger.WriteLog("JAAGENTHOUSEKEEPER000001", configCleanupRunTime, configCleanupTime)

	for {
		logger.WriteLog("JAAGENTHOUSEKEEPER400001", fn, configCleanupRunTime, configCleanupTime)
		if runtime.GOOS == "linux" {
			select {
			case <-wm.Wm.Ctx.Done():
				logger.WriteLog("JAAGENTHOUSEKEEPER400007", fn, housekeeperId)
				return
			case <-time.After(1 * time.Second):
				wm.Wm.MonitorChan <- housekeeperId
			}
		}

		if configCleanupRunTime == 24 {
			// Use JaCleanupTime, which is an interval; perform once per JaCleanupTime hour
			remainder := time.Now().Unix() % int64(configCleanupTime*3600)
			if remainder <= int64(sleepTime) {
				logger.WriteLog("JAAGENTHOUSEKEEPER000002", fn)
				logger.WriteLog("JAAGENTHOUSEKEEPER400002", fn, configCleanupTime, remainder)
				startCleanup(agent.Options.JaJobHistory)

				if err := cleanUpRunCount(agent.Options.JaJobRunCountHistory); err != nil {
					logger.WriteLog("JAAGENTHOUSEKEEPER200011", fn, err.Error())
				}
				logger.WriteLog("JAAGENTHOUSEKEEPER000003", fn)
			}

		} else {
			// Do not use JaCleanupTime. perform backup at exactly JaBackupRunTime once
			now := time.Now()
			currentHour := now.Hour()
			currentMin := now.Minute()

			if currentMin == 0 && configCleanupRunTime == currentHour && !doneCleanup {
				logger.WriteLog("JAAGENTHOUSEKEEPER400003", fn, configCleanupRunTime, currentHour)
				doneCleanup = true // Ensure the cleanup to run only once
				logger.WriteLog("JAAGENTHOUSEKEEPER000004", fn)
				startCleanup(agent.Options.JaJobHistory)
				if err := cleanUpRunCount(agent.Options.JaJobRunCountHistory); err != nil {
					logger.WriteLog("JAAGENTHOUSEKEEPER200011", fn, err.Error())
				}
				logger.WriteLog("JAAGENTHOUSEKEEPER000005", fn)
			} else if doneCleanup && configCleanupRunTime != currentHour {
				// when configCleanupRunTime is over and executed once, set to false for next cleanup time
				doneCleanup = false
			}

		}

		time.Sleep(time.Duration(sleepTime) * time.Second)
	}

}

func startCleanup(day int) error {
	fn := "startCleanup"

	// Acquire exclusive lock on the lock file before processing.
	file, err := utils.LockFile(utils.LOCKFILE_EXCLUSIVE_LOCK | utils.LOCKFILE_FAIL_ON_LOCK)
	if err != nil {
		return fmt.Errorf("failed to acquire share lock. from dir: %s, to dir: %s, err: [%v]", "", "", err)
	}

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

	entries, err := os.ReadDir(CloseFolderPath)
	if err != nil {
		return fmt.Errorf("faild to read dir '%s': %s", CloseFolderPath, err.Error())
	}

	logger.WriteLog("JAAGENTHOUSEKEEPER400004", fn, day)
	cutoff := time.Now().AddDate(0, 0, -day)
	logger.WriteLog("JAAGENTHOUSEKEEPER400005", fn, cutoff.Unix())

	// For throttle mechanism
	waitCount := 0
	waitMax := 3

	start := time.Now().Unix()
	failCount := 0
	successCount := 0
	toErrorCount := 0
	cleanupFolderCount := 0
	cleanupFolderFailCount := 0
	cleanupFileCount := 0
	cleanupFileFailCount := 0

	for _, entry := range entries {
		entryFullPath := filepath.Join(CloseFolderPath, entry.Name())
		info, err := os.Stat(entryFullPath)
		if err != nil {
			logger.WriteLog("JAAGENTHOUSEKEEPER200010", fn, entryFullPath, err.Error())
			continue
		}

		// skip entry that not end with .job
		if !entry.IsDir() && !strings.HasSuffix(entryFullPath, ".job") {
			continue
		}

		// not over the retention period
		if !isFileOlderThan(info, day) {
			continue
		}

		// .job file
		if strings.HasSuffix(entryFullPath, ".job") {
			// ends with .job
			associatedFolderPath := strings.TrimSuffix(entryFullPath, ".job")

			_, err := os.Stat(associatedFolderPath)
			if err != nil {
				// check if the associated folder does not exist
				if os.IsNotExist(err) {
					logger.WriteLog("JAAGENTHOUSEKEEPER000006", fn, entryFullPath)
				} else {
					logger.WriteLog("JAAGENTHOUSEKEEPER200002", fn, associatedFolderPath, err.Error())
					continue
				}

				// delete .job file
				if err := os.Remove(entryFullPath); err != nil {
					logger.WriteLog("JAAGENTHOUSEKEEPER200001", fn, entryFullPath, err.Error())
					cleanupFileFailCount++
					continue
				}

				logger.WriteLog("JAAGENTHOUSEKEEPER000007", fn, entryFullPath)
				cleanupFileCount++
				continue
			}

			// associated folder exists
			entries, err := os.ReadDir(associatedFolderPath)
			if err != nil {
				logger.WriteLog("JAAGENTHOUSEKEEPER200003", fn, associatedFolderPath, err.Error())
				continue
			}

			// Check length of entries in data folder
			if len(entries) < 7 {
				// Move folder to error/
				logger.WriteLog("JAAGENTHOUSEKEEPER400006", fn, associatedFolderPath)
				destFolderPath := filepath.Join(ErrorFolderPath, filepath.Base(associatedFolderPath))
				if err := utils.MoveFileWithLockRetry(associatedFolderPath, destFolderPath, common.FileLockRetryCount); err != nil {
					logger.WriteLog("JAAGENTHOUSEKEEPER200004", fn, associatedFolderPath, err.Error())
					continue
				}

				// Move .job to error/
				destFilePath := filepath.Join(ErrorFolderPath, filepath.Base(entryFullPath))
				if err := utils.MoveFileWithLockRetry(entryFullPath, destFilePath, common.FileLockRetryCount); err != nil {
					logger.WriteLog("JAAGENTHOUSEKEEPER200005", fn, entryFullPath, err.Error())
					continue
				}

				toErrorCount++
				continue
			}

			// Throttle mechanism
			if waitCount >= waitMax {
				time.Sleep(1 * time.Second)
				waitCount = 0
			} else {
				waitCount++
			}

			// Delete data folder
			if err := os.RemoveAll(associatedFolderPath); err != nil {
				logger.WriteLog("JAAGENTHOUSEKEEPER200006", fn, associatedFolderPath, err.Error())
				failCount++
				continue
			}

			// Delete .job file
			if err := os.Remove(entryFullPath); err != nil {
				logger.WriteLog("JAAGENTHOUSEKEEPER200007", fn, entryFullPath, err.Error())
				failCount++
				continue
			}

			logger.WriteLog("JAAGENTHOUSEKEEPER000008", fn, entryFullPath)
			successCount++

		}

		// directory
		if entry.IsDir() {
			// is a directory
			jobFilePath := fmt.Sprintf("%s.job", entryFullPath)
			_, err := os.Stat(jobFilePath)
			if err == nil {
				// let the future iterations do the work
				continue
			}

			// associated job file does not exist: invalid folder
			logger.WriteLog("JAAGENTHOUSEKEEPER000009", fn, entryFullPath)

			// Throttle mechanism
			if waitCount >= waitMax {
				time.Sleep(1 * time.Second)
				waitCount = 0
			} else {
				waitCount++
			}

			// Remove the directory
			if err := os.RemoveAll(entryFullPath); err != nil {
				logger.WriteLog("JAAGENTHOUSEKEEPER200008", fn, entryFullPath, err.Error())
				cleanupFolderFailCount++
				continue
			}

			logger.WriteLog("JAAGENTHOUSEKEEPER000010", fn, entryFullPath)
			cleanupFolderCount++

		}

		logger.WriteLog("JAAGENTHOUSEKEEPER000011", fn, successCount, failCount)
		logger.WriteLog("JAAGENTHOUSEKEEPER000012", fn, cleanupFileCount, cleanupFileFailCount)
		logger.WriteLog("JAAGENTHOUSEKEEPER000013", fn, cleanupFolderCount, cleanupFolderFailCount)
		logger.WriteLog("JAAGENTHOUSEKEEPER000014", fn, toErrorCount)
		diff := time.Now().Unix() - start
		logger.WriteLog("JAAGENTHOUSEKEEPER000015", fn, diff)

	}

	// delete index job files
	successCount = 0
	failCount = 0
	indexJobEntries, err := os.ReadDir(JobsFolderPath)
	if err != nil {
		return fmt.Errorf("faild to read dir '%s': %s", JobsFolderPath, err.Error())
	}
	for _, indexEntry := range indexJobEntries {
		indexEntryFullPath := filepath.Join(JobsFolderPath, indexEntry.Name())
		if indexEntry.IsDir() {
			continue
		}

		if err := os.Remove(indexEntryFullPath); err != nil {
			logger.WriteLog("JAAGENTHOUSEKEEPER200009", fn, indexEntryFullPath, err.Error())
			failCount++
			continue
		}

		logger.WriteLog("JAAGENTHOUSEKEEPER000016", fn, indexEntryFullPath)
		successCount++

	}

	logger.WriteLog("JAAGENTHOUSEKEEPER000017", fn, successCount, failCount)

	return nil
}

func cleanUpRunCount(days int) error {
	funcName := "cleanUpRunCount"

	// Acquire exclusive lock on the lock file before processing.
	file, err := utils.LockFile(utils.LOCKFILE_EXCLUSIVE_LOCK | utils.LOCKFILE_FAIL_ON_LOCK)
	if err != nil {
		return fmt.Errorf("failed to acquire share lock. from dir: %s, to dir: %s, err: [%v]", "", "", err)
	}

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

	entries, err := os.ReadDir(RunCountFolderPath)
	if err != nil {
		return fmt.Errorf("[%s] failed to read directory: %v", funcName, err)
	}

	for _, entry := range entries {
		if !entry.IsDir() {
			// Skip files — we only want folders like <ServerID>
			continue
		}

		folderName := entry.Name()
		targetDir := filepath.Join(RunCountFolderPath, folderName)

		// Convert folderName to uint64
		id, err := strconv.ParseUint(folderName, 10, 64)
		if err != nil {
			// If folder name is not numeric, skip it
			continue
		}

		// Perform cleanup on this folder
		if err := event.DeleteOldRunCountFile(RunCountFolderPath, id, days); err != nil {
			return fmt.Errorf("[%s] cleanup failed on %s: %v", funcName, targetDir, err)
		}
	}

	return nil
}

func isFileOlderThan(info os.FileInfo, days int) bool {
	modTime := info.ModTime()
	cutoff := time.Now().AddDate(0, 0, -days)

	return modTime.Before(cutoff)
}
