/*
** 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"
	"os/exec"
	"runtime/debug"
	"strings"
	"unicode/utf8"

	clientcommon "jobarranger2/src/jobarg_server/managers/icon_exec_manager/workers/common"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/server"
	"jobarranger2/src/libs/golibs/event"
	"jobarranger2/src/libs/golibs/utils"
)

var (
	err                 error
	exitCode            int
	tmpDir, udsPath     string
	runStatusChangeFlag bool
)

func calcIconClient() (int, error) {
	var (
		eventData     common.EventData
		executionData common.IconExecutionProcessData
		calcIcon      common.IconCalcData

		innerJobnetMainId, innerJobnetId, innerJobId uint64
		jobnetId                                     string
		runCount, methodFlag, iconType               int
		flag                                         bool
	)

	executionData, err = clientcommon.GetIconExecData()
	if err != nil {
		return common.JA_JOBEXEC_FAIL, err
	}

	// Check necessary data
	innerJobnetMainId = executionData.RunJobData.InnerJobnetMainID
	if innerJobnetMainId <= 0 {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid inner_jobnet_main_id")
	}
	innerJobId = executionData.RunJobData.InnerJobID
	if innerJobId <= 0 {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid inner_job_id")
	}
	runCount = executionData.RunJobData.RunCount
	if runCount < 0 {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid run_count")
	}
	methodFlag = int(executionData.RunJobData.MethodFlag)
	if methodFlag < 0 {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid method_flag")
	}

	// check flag file exist or not
	flag, err = event.CheckRunCountFile(clientcommon.RunCountFolderPath, innerJobnetMainId, innerJobId, runCount, methodFlag)
	if err != nil {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("failed to create flag file : %w", err)
	} else if !flag { // flag == false (nil && false)
		return common.JA_JOBEXEC_IGNORE, nil
	}

	if err := utils.Convert(executionData.RunJobData.Data, &calcIcon); err != nil {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("failed to convert Data : %w", err)
	}
	if err := clientcommon.SetBeforeVariable(executionData); err != nil {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("failed to set before variable: %w", err)
	}
	jobnetId = executionData.RunJobnetData.JobnetID
	if jobnetId == "" {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid jobnet_id")
	}

	innerJobnetId = executionData.RunJobData.InnerJobnetID
	if innerJobnetId <= 0 {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid inner_jobnet_id")
	}

	iconType = int(executionData.RunJobData.IconType)
	if iconType != int(common.IconTypeCalc) {
		return common.JA_JOBEXEC_FAIL, fmt.Errorf("invalid icon_type for calc icon")
	}

	eventData = clientcommon.IconRunDataPrep(string(clientcommon.IconClientCalc), executionData)

	err = event.CreateNextEvent(eventData, innerJobnetId, jobnetId, innerJobId)
	if err != nil {
		return common.JA_JOBRESULT_PRE_EXEC_FAIL, err
	}
	runStatusChangeFlag = true

	// flag file creation
	err = event.CreateRunCountFile(clientcommon.RunCountFolderPath, innerJobnetMainId, innerJobId, runCount)
	if err != nil {
		return common.JA_JOBRESULT_FAIL, fmt.Errorf("failed to create flag file : %w", err)
	}

	// Run the command and get result
	resultStr, err := getCalcResult(calcIcon)
	if err != nil {
		return common.JA_JOBRESULT_FAIL, fmt.Errorf("failed to get calc result: %w", err)
	}

	if calcIcon.ValueName == "" {
		return common.JA_JOBRESULT_FAIL, fmt.Errorf("ValueName is empty")
	}
	calcIcon.Value = resultStr

	if err := clientcommon.WriteStructToFD3(calcIcon); err != nil {

		fmt.Fprintln(os.Stderr, "Failed to write calculation result to fd3:", err)
	}

	return common.JA_JOBRESULT_SUCCEED, nil
}

func getCalcResult(calcIcon common.IconCalcData) (string, error) {
	var cmd string
	var err error
	Timezone := clientcommon.BeforeVariableMap["TIMEZONE"]
	if calcIcon.HandFlag == 0 {
		cmd = formatExpr(calcIcon.Formula)
	} else {
		cmd, err = formatDate(calcIcon.Formula, Timezone)
		if err != nil {
			return "", fmt.Errorf("failed to format date: %w", err)
		}
	}

	envVars := setEnvFromBefore()

	output, err := executeCommand(cmd, envVars)
	if err != nil {
		return "", err
	}

	output = strings.TrimSpace(output)
	output = safeTrimUTF8(output, 4096)
	return output, nil
}

func formatExpr(input string) string {
	var builder strings.Builder
	builder.WriteString("expr ")

	escapeChars := "+-*/%!<>=()|&:;[]`"
	for _, ch := range input {
		if strings.ContainsRune(escapeChars, ch) {
			builder.WriteRune('\\')
		}
		builder.WriteRune(ch)
	}

	return builder.String()
}

func formatDate(input string, tz string) (string, error) {
	if tz == "" {
		return "", fmt.Errorf("timezone is empty")
	}

	parts := strings.SplitN(input, ";", 2)
	datePart := strings.TrimSpace(parts[0])
	formatPart := ""
	if len(parts) > 1 {
		formatPart = strings.TrimSpace(parts[1])
	}

	sanitize := func(s string) string {
		var b strings.Builder
		for _, r := range s {
			switch r {
			case '`', '\'', '>', '<', '&', '\\':
				continue
			default:
				b.WriteRune(r)
			}
		}
		return b.String()
	}

	datePart = sanitize(datePart)
	formatPart = sanitize(formatPart)

	// remove any existing quotes
	datePart = strings.Trim(datePart, "\"")
	formatPart = strings.Trim(formatPart, "\"")

	var cmdBuilder strings.Builder
	cmdBuilder.WriteString(fmt.Sprintf("TZ=%s date --date=\"%s\"", tz, datePart))
	if formatPart != "" {
		cmdBuilder.WriteString(fmt.Sprintf(" +\"%s\"", formatPart))
	}

	cmd := cmdBuilder.String()
	return cmd, nil
}

func setEnvFromBefore() []string {
	env := os.Environ()
	for k, v := range clientcommon.BeforeVariableMap {
		env = append(env, fmt.Sprintf("%s=%s", k, v))
	}
	return env
}

func executeCommand(command string, env []string) (string, error) {
	cmd := exec.Command("sh", "-c", command)
	cmd.Env = env

	outputBytes, err := cmd.CombinedOutput()
	output := strings.TrimSpace(string(outputBytes))

	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			// ONLY expr returns exit code 1 for valid result == 0
			if exitErr.ExitCode() == 1 && strings.HasPrefix(command, "expr ") {
				return output, nil
			}
		}
		return "", fmt.Errorf("command execution failed: %w", err)
	}

	return output, nil
}

func safeTrimUTF8(s string, max int) string {
	if len(s) <= max {
		return s
	}
	for i := max; i >= 0; i-- {
		if utf8.ValidString(s[:i]) {
			return s[:i]
		}
	}
	return ""
}

func main() {

	//catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			//output stacktrace
			fmt.Fprintf(os.Stderr, "[CalcIconClient] Runtime panic error occurs in client. error : %s", string(debug.Stack()))
			if runStatusChangeFlag {
				os.Exit(common.JA_JOBRESULT_FAIL)
			}
			os.Exit(common.JA_JOBRESULT_PRE_EXEC_FAIL)
		}
	}()

	// add client_pid in .clientPID file
	pid := os.Getpid()
	err := common.SetClientPid(pid)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[CalcIconClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	err = clientcommon.ParseArgs()
	if err != nil {
		fmt.Fprintf(os.Stderr, "[CalcIconClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	server.Options.UnixSockParentDir = clientcommon.UdsDirpath
	server.Options.TmpDir = clientcommon.TmpDirPath

	exitCode, err = calcIconClient()
	if err != nil {
		fmt.Fprintf(os.Stderr, "[CalcIconClient] %v", err)

	}
	os.Exit(exitCode)
}
