/*
** 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 (
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"
	"syscall"
	"time"

	"jobarranger2/src/jobarg_server/managers/icon_exec_manager/events"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/server"
	"jobarranger2/src/libs/golibs/event"
	"jobarranger2/src/libs/golibs/forker"
)

var (
	transationFileId, udsFilepath, inFolderPath, runFolderPath, endFolderPath, errFolderPath string
	retFilePath, clientDataFolderPath, udsDirpath, tmpDirPath, timeout, sourceIp             string
)

func checkCommandParams() error {
	if transationFileId == "" {
		return fmt.Errorf("-id flag is mandatory")
	}
	if udsFilepath == "" {
		return fmt.Errorf("-uds flag is mandatory")
	}
	if inFolderPath == "" {
		return fmt.Errorf("-in flag is mandatory")
	}
	if runFolderPath == "" {
		return fmt.Errorf("-run flag is mandatory")
	}
	if endFolderPath == "" {
		return fmt.Errorf("-end flag is mandatory")
	}
	if errFolderPath == "" {
		return fmt.Errorf("-error flag is mandatory")
	}
	if retFilePath == "" {
		return fmt.Errorf("-ret flag is mandatory")
	}
	if clientDataFolderPath == "" {
		return fmt.Errorf("-clientdata flag is mandatory")
	}
	if udsDirpath == "" {
		return fmt.Errorf("-udsdir flag is mandatory")
	}
	if tmpDirPath == "" {
		return fmt.Errorf("-tmpdir flag is mandatory")
	}
	if timeout == "" {
		return fmt.Errorf("-timeout flag is mandatory")
	}

	return nil
}

func forkerIconTypeCommon(execPath string) (forker.Forker, error) {
	extra_files := make([]*os.File, len(common.ClientExts))

	for idx, ext := range common.ClientExts {
		if ext == ".data" || ext == ".clientPID" {
			file, err := os.Create(filepath.Join(clientDataFolderPath, transationFileId+ext))
			if err != nil {
				return forker.Forker{}, err
			}

			extra_files[idx] = file
		}
	}

	pid := os.Getpid()
	if err := os.WriteFile(filepath.Join(clientDataFolderPath, transationFileId+common.ClientExts[5]), []byte(fmt.Sprintf("%d\n", pid)), 0644); err != nil {
		return forker.Forker{}, fmt.Errorf("failed to write helperPID: %v", err)
	}

	return forker.New(forker.ProcessData{
		ExecPath: execPath,
		ExecParams: []string{
			"-id", transationFileId,
			"-uds", udsFilepath,
			"-in", inFolderPath,
			"-run", runFolderPath,
			"-udsdir", udsDirpath,
			"-tmpdir", tmpDirPath,
			"-timeout", timeout,
			"-clientdata", clientDataFolderPath,
			"-sourceIp", sourceIp,
		},
		RetPath:    retFilePath,
		ExtraFiles: extra_files,
	}), nil
}

func getForker(processsData common.IconExecutionProcessData) (forker.Forker, error) {
	var forker forker.Forker
	var err error

	switch processsData.RunJobData.IconType {
	// icons
	case common.IconTypeLess:
		forker, err = forkerIconTypeCommon(common.LessIconClientExecPath)
	case common.IconTypeStart:
		forker, err = forkerIconTypeCommon(common.StartIconClientExecPath)
	case common.IconTypeEnd:
		forker, err = forkerIconTypeCommon(common.EndIconClientExecPath)
	case common.IconTypeJob:
		forker, err = forkerIconTypeCommon(common.JobIconClientExecPath)
	case common.IconTypeExtJob:
		forker, err = forkerIconTypeCommon(common.ExtJobIconClientExecPath)
	case common.IconTypeIf:
		forker, err = forkerIconTypeCommon(common.IfIconClientExecPath)
	case common.IconTypeIfEnd:
		forker, err = forkerIconTypeCommon(common.IfEndIconClientExecPath)
	case common.IconTypeW:
		forker, err = forkerIconTypeCommon(common.WIconClientExecPath)
	case common.IconTypeM:
		forker, err = forkerIconTypeCommon(common.MIconClientExecPath)
	case common.IconTypeValue:
		forker, err = forkerIconTypeCommon(common.ValueIconClientExecPath)
	case common.IconTypeL:
		forker, err = forkerIconTypeCommon(common.LoopIconClientExecPath)
	case common.IconTypeCalc:
		forker, err = forkerIconTypeCommon(common.CalculationIconClientExecPath)
	case common.IconTypeInfo:
		forker, err = forkerIconTypeCommon(common.InfoIconClientExecPath)
	case common.IconTypeFCopy:
		forker, err = forkerIconTypeCommon(common.FCopyIconClientExecPath)
	case common.IconTypeFWait:
		forker, err = forkerIconTypeCommon(common.FWaitIconClientExecPath)
	case common.IconTypeJobnet:
		forker, err = forkerIconTypeCommon(common.JobnetIconClientExecPath)
	case common.IconTypeLink:
		forker, err = forkerIconTypeCommon(common.LinkIconClientExecPath)
	case common.IconTypeRel:
		forker, err = forkerIconTypeCommon(common.RelIconClientExecPath)
	case common.IconTypeTask:
		forker, err = forkerIconTypeCommon(common.TaskIconClientExecPath)
	case common.IconTypeReboot:
		forker, err = forkerIconTypeCommon(common.RebootIconClientExecPath)

	// check job
	case common.IconTypeCheckJob:
		forker, err = forkerIconTypeCommon(common.CheckClientExecPath)
	default:
		err = fmt.Errorf("unknown icon type: %d", processsData.RunJobData.IconType)
	}

	return forker, err
}

func exit(exitCode int, processData *common.IconExecutionProcessData) {
	if processData == nil {
		fmt.Fprintln(os.Stderr, "processData is nil, exiting")
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	// read client_data
	var err error
	var data []byte
	resultMap := make(map[string][]byte, len(common.ClientExts))

	for _, ext := range common.ClientExts {
		filePath := filepath.Join(clientDataFolderPath, transationFileId+ext)

		// dont need to read ret file. we already got exit code
		if ext == common.ClientReturnExt {
			data = []byte(strconv.Itoa(exitCode))
			goto SET_RESULT_MAP
		}

		data, err = os.ReadFile(filePath)
		if err != nil {
			if ext == common.ClientStderrExt {
				resultMap[ext] = []byte(fmt.Sprintf("could not get stderr, err: %v", err))
			} else {
				fmt.Fprintf(os.Stderr, "could not get %s, filepath: %s, err: %v\n", ext, filePath, err)
			}
			continue
		}

SET_RESULT_MAP:
		resultMap[ext] = data
	}

	eventData := common.EventData{
		Event: common.Event{
			Name:      common.EventIconExecEnd,
			UniqueKey: common.GetUniqueKey(common.IconExecManagerProcess),
		},
		NextProcess: common.NextProcess{
			Name: common.IconExecManagerProcess,
			Data: common.IconRunData{
				TransactionFileId: transationFileId,
				ExecProcessData:   *processData,
				ClientData:        resultMap,
			},
		},
	}

	err = event.CreateNextEvent(
		eventData, processData.RunJobData.InnerJobnetID,
		processData.RunJobnetData.JobnetID, processData.RunJobData.InnerJobID,
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, "CreateNextEvent failed, err:", err.Error())
		exitCode = common.JA_JOBRESULT_FAIL
	}

	os.Exit(exitCode)
}

func main() {

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

	// command parameters
	flag.StringVar(&transationFileId, "id", "", "icon transaction file's id")
	flag.StringVar(&udsFilepath, "uds", "", "unix domain socket filepath")
	flag.StringVar(&inFolderPath, "in", "", "exec manager's in folderpath")
	flag.StringVar(&runFolderPath, "run", "", "exec manager's run folderpath")
	flag.StringVar(&endFolderPath, "end", "", "exec manager's end folderpath")
	flag.StringVar(&errFolderPath, "error", "", "exec manager's error folderpath")
	flag.StringVar(&clientDataFolderPath, "clientdata", "", "client_data folderpath")
	flag.StringVar(&retFilePath, "ret", "", "ret file path")
	flag.StringVar(&udsDirpath, "udsdir", "", "uds parent folderpath")
	flag.StringVar(&tmpDirPath, "tmpdir", "", "tmp folderpath")
	flag.StringVar(&timeout, "timeout", "0", "timeout")
	flag.StringVar(&sourceIp, "sourceIp", "", "sourceIp")

	// parse flags
	flag.Parse()

	// check command parameters
	err := checkCommandParams()
	if err != nil {
		fmt.Fprintln(os.Stderr, "checkCommandParams failed, err:", err.Error())
		exit(common.JA_JOBEXEC_FAIL, nil)
	}

	// since client_helper does not read config file, some fields need to be assigin manual
	server.Options.UnixSockParentDir = udsDirpath
	server.Options.TmpDir = tmpDirPath

	// get processData
	processData, err := events.GetIconExecutionProcessData(
		filepath.Join(inFolderPath, transationFileId+".json"))
	if err != nil {
		fmt.Fprintln(os.Stderr, "GetIconExecutionProcessData failed, err:", err.Error())
		exit(common.JA_JOBEXEC_FAIL, processData)
	}

	// --- Set up signal handler (USR1) ---
	exitFlag := make(chan bool, 1)
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGUSR1)

	go func() {
		<-sigChan // wait incoming signal from icon run event
		exitFlag <- true
	}()

	// get forker, includes client_data files creation
	forker, err := getForker(*processData)
	if err != nil {
		fmt.Fprintln(os.Stderr, "getForker() failed, err:", err.Error())
		exit(common.JA_JOBEXEC_FAIL, processData)
	}

	// child process fork
	cmd, err := forker.StartNewProcess()
	if err != nil {
		fmt.Fprintln(os.Stderr, "StartNewProcess() failed, err:", err.Error())
		exit(common.JA_JOBEXEC_FAIL, processData)
	}

	exitCode := 1
	var byteData []byte

	// wait child process exit
	err = cmd.Wait()
	if err != nil {
		fmt.Fprintln(os.Stderr, "cmd.Wait() failed, err:", err.Error())
		exitCode = common.JA_JOBRESULT_FAIL

		goto EXIT
	} else {
		exitCode = common.JA_JOBRESULT_SUCCEED
	}

	// get icon client exitcode
	byteData, err = os.ReadFile(retFilePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "could not get icon client exitcode, filepath: %s, err: %s\n", retFilePath, err.Error())
		exitCode = common.JA_JOBRESULT_FAIL

		goto EXIT
	}

	exitCode, err = strconv.Atoi(strings.TrimSuffix(string(byteData), "\n"))
	if err != nil {
		fmt.Fprintf(os.Stderr, "could not get icon client exitcode, filepath: %s, err: %s\n", retFilePath, err.Error())
		exitCode = common.JA_JOBRESULT_FAIL

		goto EXIT
	}

	if exitCode == common.JA_JOBRESULT_SUCCEED {
		timeout := 30 * time.Second
		timer := time.NewTimer(timeout)
		defer timer.Stop() // in case select returns early

		// --- Wait for signal with timeout ---
		select {
		case <-exitFlag:
			// received signal — proceed
		case <-timer.C:
			// timeout — proceed
			fmt.Printf("exitFlag timeout reached (%v sec)\n", int(timeout.Seconds()))
		}
	}

EXIT:
	exit(exitCode, processData)
}
