/*
** 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"
	"os/exec"
	"strconv"
	"strings"
	"time"

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

var (
	paramInfo paramsInfo
)

type zbxHostInfo struct {
	Ip       string `json:"zabbix_server_ip_address"`
	Port     string `json:"zabbix_server_port_number"`
	HostName string `json:"message_destination_hostname"`
	Item     string `json:"message_destination_item_key"`
}

type paramsInfo struct {
	zbxHosts      []zbxHostInfo
	retry         int
	retryCnt      int
	retryInterval int
	zbxFlag       int
	zbxSendCmd    string
}

func getConfParameters(dbConn database.DBConnection) error {
	// Clear paramInfo
	paramInfo = paramsInfo{}

	// Inline function to fetch rows
	fetchAllRows := func(query string) ([]map[string]string, error) {
		dbResult, err := dbConn.Select(query)
		if err != nil {
			return nil, err
		}

		// Free db result
		defer dbResult.Free()

		var rows []map[string]string
		for dbResult.HasNextRow() {
			row, err := dbResult.Fetch()
			if err != nil {
				return nil, err
			}
			rows = append(rows, row)
		}
		return rows, nil
	}

	// Load Zabbix hosts
	hostRows, err := fetchAllRows("SELECT data FROM ja_2_zbx_hosts_table")
	if err != nil {
		return err
	}
	for _, row := range hostRows {
		var host zbxHostInfo
		if err := json.Unmarshal([]byte(row["data"]), &host); err != nil {
			return err
		}
		paramInfo.zbxHosts = append(paramInfo.zbxHosts, host)
	}

	// Load other config parameters
	paramMap := map[int]string{
		1: "ZBXSND_RETRY",
		2: "ZBXSND_RETRY_COUNT",
		3: "ZBXSND_RETRY_INTERVAL",
		4: "ZBXSND_ON",
		5: "ZBXSND_SENDER",
	}

	for i := 1; i <= 5; i++ {
		paramName := paramMap[i]
		rows, err := fetchAllRows(fmt.Sprintf(
			"SELECT value FROM ja_2_parameter_table WHERE parameter_name = '%s'", paramName))
		if err != nil {
			return err
		}
		if len(rows) == 0 {
			continue
		}

		val := rows[0]["value"]
		switch i {
		case 1:
			if v, err := strconv.Atoi(val); err == nil && (v == 0 || v == 1) {
				paramInfo.retry = v
			}
		case 2:
			if v, err := strconv.Atoi(val); err == nil && v >= 0 {
				paramInfo.retryCnt = v
			}
		case 3:
			if v, err := strconv.Atoi(val); err == nil && v >= 0 {
				paramInfo.retryInterval = v
			}
		case 4:
			if v, err := strconv.Atoi(val); err == nil && v >= 0 {
				paramInfo.zbxFlag = v
			}
		case 5:
			paramInfo.zbxSendCmd = val
		}
	}

	return nil
}

func findParentJobID(dbConn database.DBConnection, targetJobnetID uint64) (uint64, error) {

	query := fmt.Sprintf(`
		SELECT inner_job_id, data 
		FROM ja_2_run_job_table 
		WHERE job_type = %d`, common.IconTypeJobnet)

	dbResult, err := dbConn.Select(query)
	if err != nil {
		return 0, err
	}

	// Free db result
	defer dbResult.Free()

	for dbResult.HasNextRow() {
		row, err := dbResult.Fetch()
		if err != nil {
			return 0, err
		}

		var detail common.IconDetailData
		if err := json.Unmarshal([]byte(row["data"]), &detail); err != nil {
			return 0, err
		}

		if detail.LinkInnerJobnetID == targetJobnetID {
			return strconv.ParseUint(row["inner_job_id"], 10, 64)
		}
	}

	return 0, nil
}

func getJobIdFull(dbConn database.DBConnection, innerJobID uint64) (string, error) {
	if innerJobID == 0 {
		return "", nil
	}

	// Inline function fetch job row by inner_job_id
	fetchJobRow := func(id uint64) (map[string]string, error) {
		query := fmt.Sprintf(`
			SELECT inner_jobnet_id, inner_jobnet_main_id, job_id 
			FROM ja_2_run_job_table WHERE inner_job_id = %d`, id)

		dbResult, err := dbConn.Select(query)
		if err != nil {
			return nil, err
		}

		// Free db result
		defer dbResult.Free()

		if !dbResult.HasNextRow() {
			return nil, nil
		}

		return dbResult.Fetch()
	}

	// Inline function fetch jobnet_id by inner_jobnet_id
	fetchJobnetID := func(id uint64) (string, error) {
		query := fmt.Sprintf(`
			SELECT jobnet_id FROM ja_2_run_jobnet_table 
			WHERE inner_jobnet_id = %d`, id)

		dbResult, err := dbConn.Select(query)
		if err != nil {
			return "", err
		}

		// Free db result
		defer dbResult.Free()

		if !dbResult.HasNextRow() {
			return "", nil
		}

		row, err := dbResult.Fetch()
		if err != nil {
			return "", err
		}
		return row["jobnet_id"], nil
	}

	fullParts := []string{}
	wInnerJobID := innerJobID
	var mainJobnetID uint64

	// Loop until reaching main jobnet
	for {
		row, err := fetchJobRow(wInnerJobID)
		if err != nil {
			return "", err
		}
		if row == nil {
			break
		}

		jobID := row["job_id"]
		innerJobnetIDStr := row["inner_jobnet_id"]
		innerMainStr := row["inner_jobnet_main_id"]

		innerJobnetID, _ := strconv.ParseUint(innerJobnetIDStr, 10, 64)
		innerJobnetMainID, _ := strconv.ParseUint(innerMainStr, 10, 64)

		// Append part
		fullParts = append([]string{jobID}, fullParts...)

		// Found main jobnet?
		if innerJobnetID == innerJobnetMainID {
			mainJobnetID = innerJobnetMainID
			break
		}

		// Find parent jobnet icon
		parentID, err := findParentJobID(dbConn, innerJobnetID)
		if err != nil {
			return "", err
		}
		if parentID == 0 {
			break
		}

		wInnerJobID = parentID
	}

	// Prepend main jobnet id
	if mainJobnetID != 0 {
		mainIDStr, err := fetchJobnetID(mainJobnetID)
		if err != nil {
			return "", err
		}
		if mainIDStr != "" {
			fullParts = append([]string{mainIDStr}, fullParts...)
		}
	}

	return strings.Join(fullParts, "/"), nil
}

func getJobData(dbConn database.DBConnection, sendInfo common.ZbxSendInfo, sendMsgData *common.SendMessageTable) error {

	// Inline function to fetch data
	fetchRow := func(query string) (map[string]string, error) {
		dbResult, err := dbConn.Select(query)
		if err != nil {
			return nil, err
		}
		defer dbResult.Free()

		// Free db result
		if !dbResult.HasNextRow() {
			return nil, nil
		}

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

	// Get inner_jobnet_id
	if sendInfo.InnerJobnetID == 0 && sendInfo.InnerJobID != 0 {
		query := fmt.Sprintf("SELECT inner_jobnet_id FROM ja_2_run_job_table WHERE inner_job_id = %d", sendInfo.InnerJobID)
		row, err := fetchRow(query)
		if err != nil {
			return err
		}
		if row != nil {
			if err := utils.MapStringStringToStruct(row, sendMsgData); err != nil {
				return err
			}
		}
	} else {
		sendMsgData.InnerJobnetID = sendInfo.InnerJobnetID
	}

	// Get job_id_full, job_type, job_name
	if sendInfo.InnerJobID != 0 {
		jobIdFull, err := getJobIdFull(dbConn, sendInfo.InnerJobID)
		if err != nil {
			return err
		}
		sendMsgData.JobIdFull = jobIdFull

		query := fmt.Sprintf("SELECT job_type,job_id,job_name FROM ja_2_run_job_table WHERE inner_job_id = %d", sendInfo.InnerJobID)
		row, err := fetchRow(query)
		if err != nil {
			return err
		}
		if row != nil {
			if err := utils.MapStringStringToStruct(row, sendMsgData); err != nil {
				return err
			}

			jobType, err := strconv.Atoi(row["job_type"])
			if err != nil {
				return err
			}

			// 2-1. IconType jobs
			if jobType == int(common.IconTypeJob) ||
				jobType == int(common.IconTypeFCopy) ||
				jobType == int(common.IconTypeFWait) ||
				jobType == int(common.IconTypeReboot) ||
				jobType == int(common.IconTypeLess) {

				utils.GetIconData(dbConn, sendInfo.InnerJobID)

				query = fmt.Sprintf("SELECT data FROM ja_2_run_job_table WHERE inner_job_id = %d", sendInfo.InnerJobID)
				row2, err := fetchRow(query)
				if err != nil {
					return err
				}
				if row2 != nil {
					var jobData common.IconJobData
					if err := json.Unmarshal([]byte(row2["data"]), &jobData); err != nil {
						return err
					}

					if jobData.HostFlag == 0 {
						sendMsgData.HostName = jobData.HostName
					} else {
						// Lookup before_value table
						query = fmt.Sprintf("SELECT before_variable FROM ja_2_run_job_variable_table WHERE inner_job_id = %d",
							sendInfo.InnerJobID)
						row3, err := fetchRow(query)
						if err != nil {
							return err
						}

						if row3 != nil {
							jsonStr := row3["before_variable"]

							var vars map[string]any
							if err := json.Unmarshal([]byte(jsonStr), &vars); err != nil {
								return err
							}

							// Get hostname
							sendMsgData.HostName, _ = vars[jobData.HostName].(string)
						}
					}
				}
			}
		}
	}

	// Get jobnet info
	if sendMsgData.InnerJobnetID != 0 || sendMsgData.JobnetId != "" {
		if sendMsgData.InnerJobnetID != 0 {
			query := fmt.Sprintf("SELECT jobnet_id,user_name,jobnet_name,inner_jobnet_main_id FROM ja_2_run_jobnet_table WHERE inner_jobnet_id = %d",
				sendMsgData.InnerJobnetID)
			row, err := fetchRow(query)
			if err != nil {
				return err
			}
			if row != nil {
				if err := utils.MapStringStringToStruct(row, sendMsgData); err != nil {
					return err
				}
			}

			// Sub-jobnet → fetch main jobnet
			if sendMsgData.InnerJobnetID != sendMsgData.InnerJobnetMainID {
				query = fmt.Sprintf("SELECT jobnet_id,user_name,jobnet_name FROM ja_2_run_jobnet_table WHERE inner_jobnet_id = %d",
					sendMsgData.InnerJobnetMainID)
				row2, err := fetchRow(query)
				if err != nil {
					return err
				}
				if row2 != nil {
					if err := utils.MapStringStringToStruct(row2, sendMsgData); err != nil {
						return err
					}
				}
			}

		} else {
			// Fallback to control table
			query := fmt.Sprintf("SELECT jobnet_id,user_name,jobnet_name FROM ja_2_jobnet_control_table WHERE jobnet_id = '%s' AND valid_flag = 1",
				sendMsgData.JobnetId)
			row, err := fetchRow(query)
			if err != nil {
				return err
			}
			if row != nil {
				if err := utils.MapStringStringToStruct(row, sendMsgData); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func escapeSQLString(s string) string {
	return strings.ReplaceAll(s, "'", "''")
}

func ProcessEventData(data common.Data) {
	const (
		IN    string = "in"
		RUN   string = "run"
		WAIT  string = "wait"
		END   string = "end"
		ERROR string = "error"
	)

	var (
		eventData   common.EventData
		sendInfo    common.ZbxSendInfo
		sendMsgData common.SendMessageTable
	)

	workerId := common.NotificationManagerProcess + ": ProcessEventData"

	// Unmarshal event data
	if err := utils.UnmarshalEventData(data.EventData, &eventData); err != nil {
		logger.JaLog("JANOTIFICATION200006", logger.Logging{}, workerId, err)
		return
	}

	if len(eventData.Transfer.Files) == 0 {
		logger.JaLog("JANOTIFICATION200012", logger.Logging{}, workerId, eventData.Event.UniqueKey)
		return
	}

	sourceFile := eventData.Transfer.Files[0]
	srcPath := sourceFile.Source

	// Remove original file from transfer list
	eventData.Transfer.Files = eventData.Transfer.Files[1:]

	if err := utils.Convert(eventData.NextProcess.Data, &sendInfo); err != nil {
		logger.JaLog("JANOTIFICATION200006", logger.Logging{}, workerId, err)
		err := utils.MoveToSubFolder(srcPath, ERROR)
		if err != nil {
			logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
		}
		return
	}

	// Check Zabbix notification flag
	if paramInfo.zbxFlag != 1 {
		logger.JaLog("JANOTIFICATION000002", logger.Logging{}, workerId)
		err := utils.MoveToSubFolder(srcPath, END)
		if err != nil {
			logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
		}
		return
	}

	// Check if Zabbix hosts exist
	if len(paramInfo.zbxHosts) == 0 {
		logger.JaLog("JANOTIFICATION000001", logger.Logging{}, workerId)
		err := utils.MoveToSubFolder(srcPath, END)
		if err != nil {
			logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
		}
		return
	}

	// Fetch job & jobnet data
	if err := getJobData(data.DBConn, sendInfo, &sendMsgData); err != nil {
		logger.JaLog("JANOTIFICATION200008", logger.Logging{}, workerId, err)
		err := utils.MoveToSubFolder(srcPath, ERROR)
		if err != nil {
			logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
		}
		return
	}

	logMessage := escapeSQLString(sendInfo.LogMessage)
	nowTime := time.Now().UTC().Unix()

	// Prepare queries for all Zabbix hosts
	for _, host := range paramInfo.zbxHosts {
		eventData.AddPQ("INSERT INTO ja_2_send_message_table ( message_date, inner_job_id, inner_jobnet_id, inner_jobnet_main_id, send_status, retry_count, retry_date, send_date, send_error_date,	message_type, user_name, host_name, jobnet_id, jobnet_name,	job_id, job_id_full, job_name, log_message_id, log_message, 	zbx_ip, zbx_port, zbx_host, zbx_item ) VALUES ( ?, ?, ?, ?, ?, 0, 0, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )",
			nowTime, sendInfo.InnerJobID, sendMsgData.InnerJobnetID, sendMsgData.InnerJobnetMainID,
			0,
			sendInfo.LogLevel, sendMsgData.UserName, sendMsgData.HostName, sendMsgData.JobnetId, sendMsgData.JobnetName,
			sendMsgData.JobId, sendMsgData.JobIdFull, sendMsgData.JobName,
			sendInfo.LogMessageID, logMessage,
			host.Ip, host.Port, host.HostName, host.Item)
	}

	// Create next event
	eventData.Event.Name = common.EventInsertSendMsgTable
	eventData.Event.UniqueKey = common.GetUniqueKey(common.NotificationManagerProcess)
	if err := event.CreateNextEvent(eventData, sendInfo.InnerJobnetID, sendMsgData.JobnetId, sendInfo.InnerJobID); err != nil {
		logger.JaLog("JANOTIFICATION200010", logger.Logging{}, workerId, err)
		err := utils.MoveToSubFolder(srcPath, ERROR)
		if err != nil {
			logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
		}
		return
	}

	err := utils.MoveToSubFolder(srcPath, END)
	if err != nil {
		logger.JaLog("JANOTIFICATION200011", logger.Logging{}, workerId, err)
	}
}

func notifyZabbix(db database.Database) {
	workerId := "notifyZabbix"

	dbConn, err := db.GetConn()
	if err != nil {
		logger.JaLog("JANOTIFICATION200001", logger.Logging{}, workerId, err)
		return
	}

	// Inline function to fetch rows
	fetchRows := func(query string) ([]map[string]string, error) {
		dbResult, err := dbConn.Select(query)
		if err != nil {
			return nil, err
		}

		// Free db result
		defer dbResult.Free()

		var rows []map[string]string
		for dbResult.HasNextRow() {
			row, err := dbResult.Fetch()
			if err != nil {
				return nil, err
			}
			rows = append(rows, row)
		}
		return rows, nil
	}

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

		// Reload configuration
		if err := getConfParameters(dbConn); err != nil {
			logger.JaLog("JANOTIFICATION200004", logger.Logging{}, workerId, err)
			continue
		}

		// Query pending messages
		query := fmt.Sprintf(`
			SELECT send_no, message_date, send_status, retry_count, retry_date,
			       message_type, user_name, host_name, jobnet_id, jobnet_name, job_id,
			       job_id_full, job_name, log_message_id, log_message,
			       inner_jobnet_id, inner_jobnet_main_id,
			       zbx_ip, zbx_port, zbx_host, zbx_item
			FROM ja_2_send_message_table
			WHERE (send_status = %d OR send_status = %d) AND retry_date <= %d
			ORDER BY retry_date, message_date`,
			common.JA_SNT_SEND_STATUS_BEGIN, common.JA_SNT_SEND_STATUS_RETRY, time.Now().Unix())

		rows, err := fetchRows(query)
		if err != nil {
			logger.JaLog("JANOTIFICATION200002", logger.Logging{}, workerId, err)
			continue
		}

		for _, row := range rows {
			var sendMsgInfo common.SendMessageTable
			if err := utils.MapStringStringToStruct(row, &sendMsgInfo); err != nil {
				logger.JaLog("JANOTIFICATION200003", logger.Logging{}, workerId, err)
				continue
			}

			nowTimeStr := time.Now().Format("2006/01/02 15:04:05")

			msgTypeMap := map[int]string{
				0: "INFO",
				1: "CRIT",
				2: "ERROR",
				3: "WARN",
			}
			msgType := msgTypeMap[sendMsgInfo.MessageType]
			if msgType == "" {
				msgType = "DEBUG"
			}

			sendMsgInfo.HostName = strings.TrimSpace(sendMsgInfo.HostName)
			sendMsgInfo.ZbxHost = strings.TrimSpace(sendMsgInfo.ZbxHost)
			if sendMsgInfo.HostName == "" {
				sendMsgInfo.HostName = "JAZServer"
			}
			if sendMsgInfo.ZbxHost == "" {
				sendMsgInfo.ZbxHost = sendMsgInfo.HostName
			}

			args := []string{
				"-z", sendMsgInfo.ZbxIp,
				"-p", sendMsgInfo.ZbxPort,
				"-s", sendMsgInfo.ZbxHost,
				"-k", sendMsgInfo.ZbxItem,
				"-o", fmt.Sprintf("[%s] [%s] [%s] %s (USER NAME=%s HOST=%s JOBNET=%s JOB=%s INNER_JOBNET_MAIN_ID=%d)",
					nowTimeStr, msgType, sendMsgInfo.LogMessageId, sendMsgInfo.LogMessage,
					sendMsgInfo.UserName, sendMsgInfo.HostName, sendMsgInfo.JobnetId,
					sendMsgInfo.JobIdFull, sendMsgInfo.InnerJobnetMainID),
			}

			cmd := exec.Command(paramInfo.zbxSendCmd, args...)
			output, err := cmd.CombinedOutput()
			outputStr := string(output)

			retryDate := time.Now().Unix()
			var updateQuery string
			var argValue []any

			if err != nil && !strings.Contains(outputStr, "processed: 1; failed: 0") {
				retryTime := time.Now().Add(time.Duration(paramInfo.retryInterval) * time.Second)
				retryDate = retryTime.Unix()

				switch {
				case paramInfo.retry == 1 && paramInfo.retryCnt == 0:
					// infinite retry
					updateQuery = "UPDATE ja_2_send_message_table SET send_status=?, retry_date=? WHERE send_no=?"
					argValue = []any{common.JA_SNT_SEND_STATUS_RETRY, retryDate, sendMsgInfo.SendNo}

				case paramInfo.retry == 0 || (paramInfo.retry == 1 && sendMsgInfo.RetryCount >= paramInfo.retryCnt):
					// retry limit hit
					updateQuery = "UPDATE ja_2_send_message_table SET send_status=?, send_error_date=? WHERE send_no=?"
					argValue = []any{common.JA_SNT_SEND_STATUS_ERROR, retryDate, sendMsgInfo.SendNo}
				default:
					sendMsgInfo.RetryCount++
					updateQuery = "UPDATE ja_2_send_message_table SET send_status=?, retry_count=?, retry_date=? WHERE send_no=?"
					argValue = []any{common.JA_SNT_SEND_STATUS_RETRY, sendMsgInfo.RetryCount, retryDate, sendMsgInfo.SendNo}
				}

				logger.JaLog("JANOTIFICATION200005", logger.Logging{}, workerId, cmd, outputStr, err)
			} else {
				updateQuery = "UPDATE ja_2_send_message_table SET send_status=?, send_date=? WHERE send_no=?"
				argValue = []any{common.JA_SNT_SEND_STATUS_END, retryDate, sendMsgInfo.SendNo}

				logger.JaLog("JANOTIFICATION000003", logger.Logging{}, workerId, cmd, outputStr)
			}

			// Create event in dbsyncer
			var eventData common.EventData
			eventData.Event.UniqueKey = common.GetUniqueKey(common.NotificationManagerProcess)
			eventData.Event.Name = common.EventInsertSendMsgTable
			eventData.AddPQ(updateQuery, argValue...)
			event.CreateNextEvent(eventData, sendMsgInfo.InnerJobnetID, sendMsgInfo.JobnetId, sendMsgInfo.InnerJobID)
		}

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

func StartDaemonWorkers(data common.Data) {
	wm.StartWorker(func() { notifyZabbix(data.DB) }, "notifyZabbix", 300, 0, string(common.NotificationManagerProcess))
}
