/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/
package main

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"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"
)

type CheckProcessInfo struct {
	sqlFlag  int
	jobCount int
	jobList  []uint64
}

var (
	checkProcInfo                           CheckProcessInfo
	hostFlagVali, hostNameVali, currentTime string
	targetTime                              = server.Options.InitialStartTime * 1e9
)

func init() {
	switch server.Options.DBType {
	case database.PostgresDBType:
		hostFlagVali = "(data->>'host_flag')::int"
		hostNameVali = "(data->>'host_name')::varchar"
		currentTime = "(EXTRACT(EPOCH FROM now()) * 1e9)::bigint"
	case database.MariaDBType, database.MysqlDBType:
		hostFlagVali = "JSON_EXTRACT(data, '$.host_flag')"
		hostNameVali = "JSON_EXTRACT(data, '$.host_name')"
		currentTime = "UNIX_TIMESTAMP(NOW(6)) * 1e9"
	}
}

func checkLocalProcess(db database.Database) {
	funcName := "checkLocalProcess"
	var logData logger.Logging
	logger.JaLog("JACHECKJOB400008", logData, funcName)
	var localJobList []uint64
	var innerJobId, innerJobnetId uint64

	dbConLocal, err := db.GetConn()
	if err != nil {
		logger.JaLog("JACHECKJOB200009", logData, funcName, err.Error())
		return
	}

	// ensure connection is closed when this function returns
	defer dbConLocal.Close()

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JACHECKJOB400009", logData, funcName, CheckLocalProcessWorkerID)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- CheckLocalProcessWorkerID
		}

		localJobList = []uint64{}

		query := fmt.Sprintf(
			"SELECT inner_job_id, inner_jobnet_id FROM ja_2_run_job_table WHERE "+
				"status IN (%d, %d) AND (%s - start_time) >= %d AND job_type NOT IN (%d, %d, %d, %d, %d)",
			common.StatusAbort, common.StatusRun, currentTime, targetTime, common.IconTypeJob,
			common.IconTypeFWait, common.IconTypeReboot, common.IconTypeJobnet, common.IconTypeM,
		)

		dbResult, err := dbConLocal.Select(query)
		if err != nil {
			logger.JaLog("JACHECKJOB200001", logData, funcName, "client_pid", err.Error())
			time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
			continue
		}

		for dbResult.HasNextRow() {
			row, err := dbResult.Fetch()
			if err != nil {
				logger.JaLog("JACHECKJOB200002", logData, funcName, "client_pid", err.Error())
				continue
			}

			innerJobId, err = strconv.ParseUint(row["inner_job_id"], 10, 64)
			if err != nil {
				logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_job_id", err.Error())
				continue
			}

			innerJobnetId, err = strconv.ParseUint(row["inner_jobnet_id"], 10, 64)
			if err != nil {
				logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_jobnet_id", err.Error())
				continue
			}
			localJobList = append(localJobList, innerJobId)
		}
		if len(localJobList) > 0 {
			err = prepEveDataCheckProcess(common.EventFlowCheckLocal, innerJobnetId, innerJobId, common.FlowManagerProcess,
				common.FlowProcessData{
					Data: common.CheckJobData{
						CheckJobIdList: localJobList,
					},
				})
			if err != nil {
				logger.JaLog("JACHECKJOB200006", logData, funcName, err.Error())
				time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
				continue
			}
		}
		dbResult.Free()
		time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
	}
}

func checkJobProcess(db database.Database) {
	funcName := "checkJobProcess"
	var logData logger.Logging
	var query string
	var hostname string
	var innerJobId, innerJobnetId uint64

	dbConJob, err := db.GetConn()
	if err != nil {
		logger.JaLog("JACHECKJOB200009", logData, funcName, err.Error())
		return
	}
	defer dbConJob.Close()

	logger.JaLog("JACHECKJOB400001", logger.Logging{}, funcName)

	// init slices
	if checkProcInfo.jobList == nil {
		checkProcInfo.jobList = []uint64{}
	}

	for {
		select {
		case <-wm.Wm.Ctx.Done():
			logger.JaLog("JACHECKJOB400009", logData, funcName, CheckJobProcessWorkerID)
			return
		case <-time.After(1 * time.Second):
			wm.Wm.MonitorChan <- CheckJobProcessWorkerID
		}

		checkProcInfo.sqlFlag = 0

		for {
			hostname = ""
			checkProcInfo.jobCount = 0
			checkProcInfo.jobList = []uint64{}
			var previousInnerJobnetId uint64
			var previousInnerJobId uint64

			// -----------------------
			// sqlFlag == 0 branch
			// -----------------------
			if checkProcInfo.sqlFlag == 0 {
				query = fmt.Sprintf(
					"SELECT data, inner_job_id, inner_jobnet_id, job_type FROM ja_2_run_job_table "+
						"WHERE job_type IN(%d, %d, %d) AND status IN(%d, %d) AND %s = 0 AND (%s - start_time) >= %d ORDER BY %s ASC",
					common.IconTypeJob, common.IconTypeFWait, common.IconTypeReboot,
					common.StatusAbort, common.StatusRun,
					hostFlagVali, currentTime, targetTime, hostNameVali)

				dbResult, err := dbConJob.Select(query)
				if err != nil {
					logger.JaLog("JACHECKJOB200001", logData, funcName, "data", err.Error())
					time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
					continue
				}

				for dbResult.HasNextRow() {
					row, err := dbResult.Fetch()
					if err != nil {
						logger.JaLog("JACHECKJOB200002", logData, funcName, "data", err.Error())
						continue
					}

					jobType, err := strconv.Atoi(row["job_type"])
					if err != nil {
						logger.JaLog("JACHECKJOB200003", logData, funcName, "int", "job_type", err.Error())
						continue
					}

					dataField := row["data"]

					var fetchedHostname string
					switch jobType {
					case int(common.IconTypeJob):
						var jobIconData common.IconJobData
						if err := json.Unmarshal([]byte(dataField), &jobIconData); err == nil {
							fetchedHostname = jobIconData.HostName
						} else {
							logger.JaLog("JACHECKJOB200004", logData, funcName, "Job Icon", err.Error())
							continue
						}
					case int(common.IconTypeFWait):
						var fWaitIconData common.IconFWaitData
						if err := json.Unmarshal([]byte(dataField), &fWaitIconData); err == nil {
							fetchedHostname = fWaitIconData.HostName
						} else {
							logger.JaLog("JACHECKJOB200004", logData, funcName, "FWait Icon", err.Error())
							continue
						}
					case int(common.IconTypeReboot):
						var rebootIconData common.IconRebootData
						if err := json.Unmarshal([]byte(dataField), &rebootIconData); err == nil {
							fetchedHostname = rebootIconData.HostName
						} else {
							logger.JaLog("JACHECKJOB200004", logData, funcName, "FWait Icon", err.Error())
							continue
						}
					}

					logger.JaLog("JACHECKJOB400002", logData, funcName, fetchedHostname, checkProcInfo.sqlFlag)

					innerJobId, err = strconv.ParseUint(row["inner_job_id"], 10, 64)
					if err != nil {
						logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_job_id", err.Error())
						continue
					}
					innerJobnetId, err = strconv.ParseUint(row["inner_jobnet_id"], 10, 64)
					if err != nil {
						logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_jobnet_id", err.Error())
						continue
					}

					if hostname == "" {
						hostname = fetchedHostname
					}

					if hostname != fetchedHostname {
						if len(checkProcInfo.jobList) > 0 {
							if err := prepEveDataCheckProcess(common.EventFlowCheckAgent, previousInnerJobnetId, previousInnerJobId,
								common.FlowManagerProcess, common.FlowProcessData{
									Data: common.CheckJobData{
										HostName:       hostname,
										CheckJobIdList: checkProcInfo.jobList,
									},
								}); err != nil {
								logger.JaLog("JACHECKJOB200006", logData, funcName, err.Error())
							}
							// reset for new hostname
							checkProcInfo.jobList = []uint64{}
							checkProcInfo.jobCount = 0
						}
						hostname = fetchedHostname
					}
					previousInnerJobnetId = innerJobnetId
					previousInnerJobId = innerJobId
					checkProcInfo.jobList = append(checkProcInfo.jobList, innerJobId)
					checkProcInfo.jobCount++
				}
				dbResult.Free()
			} else {
				// -----------------------
				// sqlFlag == 1 branch
				// -----------------------
				query = fmt.Sprintf(
					"SELECT va.before_variable, va.inner_job_id, va.inner_jobnet_id, jsub.host_name FROM "+
						"(SELECT inner_job_id, job_type, %s AS host_flag, %s AS host_name, start_time FROM ja_2_run_job_table j "+
						"WHERE j.job_type IN(%d, %d, %d) AND j.status IN(%d, %d)) AS jsub, "+
						"ja_2_run_job_variable_table va "+
						"WHERE jsub.inner_job_id = va.inner_job_id AND jsub.host_flag = 1 AND (%s - jsub.start_time) >= %d ORDER BY jsub.host_name ASC",
					hostFlagVali, hostNameVali,
					common.IconTypeJob, common.IconTypeFWait, common.IconTypeReboot,
					common.StatusAbort, common.StatusRun,
					currentTime, targetTime)

				dbResult, err := dbConJob.Select(query)
				if err != nil {
					logger.JaLog("JACHECKJOB200001", logData, funcName, "before_variable", err.Error())
					time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
					continue
				}

				for dbResult.HasNextRow() {
					row, err := dbResult.Fetch()
					if err != nil {
						logger.JaLog("JACHECKJOB200002", logData, funcName, "before_variable", err.Error())
						continue
					}

					innerJobId, err = strconv.ParseUint(row["inner_job_id"], 10, 64)
					if err != nil {
						logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_job_id", err.Error())
						continue
					}
					innerJobnetId, err = strconv.ParseUint(row["inner_jobnet_id"], 10, 64)
					if err != nil {
						logger.JaLog("JACHECKJOB200003", logData, funcName, "uint64", "inner_jobnet_id", err.Error())
						continue
					}

					hostnameVariable := row["host_name"]

					beforeVariable := row["before_variable"]
					var variableMap map[string]any
					if err := json.Unmarshal([]byte(beforeVariable), &variableMap); err != nil {
						logger.JaLog("JACHECKJOB200004", logData, funcName, "before variable", err.Error())
						continue
					}

					var fetchedHostname string
					hostnameVariable = strings.TrimSpace(hostnameVariable)
					hostnameVariable = strings.Trim(hostnameVariable, `"`)
					value, ok := variableMap[hostnameVariable]
					if !ok {
						logger.JaLog("JACHECKJOB200007", logData, funcName, innerJobId)
						continue
					}

					fetchedHostname, ok = value.(string)
					if !ok {
						// fallback but still safe
						logger.JaLog("JACHECKJOB300001", logData, funcName)
						fetchedHostname = fmt.Sprintf("%v", value)
					}

					logger.JaLog("JACHECKJOB400002", logData, funcName, fetchedHostname, checkProcInfo.sqlFlag)

					if hostname == "" {
						hostname = fetchedHostname
					}

					if hostname != fetchedHostname {
						if len(checkProcInfo.jobList) > 0 {
							if err := prepEveDataCheckProcess(common.EventFlowCheckAgent, previousInnerJobnetId, previousInnerJobId,
								common.FlowManagerProcess, common.FlowProcessData{
									Data: common.CheckJobData{
										HostName:       hostname,
										CheckJobIdList: checkProcInfo.jobList,
									},
								}); err != nil {
								logger.JaLog("JACHECKJOB200006", logData, funcName, err.Error())
							}
							// reset for new hostname
							checkProcInfo.jobList = []uint64{}
							checkProcInfo.jobCount = 0
						}
						hostname = fetchedHostname
					}
					previousInnerJobnetId = innerJobnetId
					previousInnerJobId = innerJobId
					checkProcInfo.jobList = append(checkProcInfo.jobList, innerJobId)
					checkProcInfo.jobCount++
				}
				dbResult.Free()
			}

			// finalize jobs
			if checkProcInfo.jobCount > 0 {
				if err := prepEveDataCheckProcess(common.EventFlowCheckAgent, innerJobnetId, innerJobId,
					common.FlowManagerProcess, common.FlowProcessData{
						Data: common.CheckJobData{
							HostName:       hostname,
							CheckJobIdList: checkProcInfo.jobList,
						},
					}); err != nil {
					logger.JaLog("JACHECKJOB200006", logData, funcName, err.Error())
				}
			}
			// toggle sqlFlag
			if checkProcInfo.sqlFlag == 0 {
				checkProcInfo.sqlFlag = 1
			} else {
				break
			}
		}

		time.Sleep(time.Duration(server.Options.JaCheckJobInterval) * time.Second)
	}
}

func prepEveDataCheckProcess[T any](eventName common.EventName, innerJobnetid uint64, innerJobId uint64, processType common.ProcessType, nextProcessData T) error {
	var eventData common.EventData

	eventData.Event.Name = eventName
	eventData.Event.UniqueKey = common.GetUniqueKey(common.RecoveryManagerProcess)
	eventData.NextProcess.Name = processType
	eventData.NextProcess.Data = nextProcessData

	err := event.CreateNextEvent(eventData, innerJobnetid, "", innerJobId)
	if err != nil {
		return err
	}
	return nil
}
