/*
** 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"
	"io"
	"os"
	"runtime/debug"
	"time"

	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 (
	runStatusChangeFlag bool
	eventData           common.EventData
	fileCopyData        common.IconFCopyData
	iconExecData        common.IconExecutionProcessData
)

func connectHosts(srcHostConn **common.NetConnection, destHostConn **common.NetConnection) error {
	var err error
	utils.Convert(iconExecData.RunJobData, &fileCopyData)

	defer func() {
		if err != nil {
			if *srcHostConn != nil {
				(*srcHostConn).Close()
			}
		}
	}()

	if *srcHostConn, err = clientcommon.TcpConnect(fileCopyData.FromHostIp, fileCopyData.FromHostPort, clientcommon.Timeout); err != nil {
		return fmt.Errorf("source host connect failed: %w", err)
	}

	if *destHostConn, err = clientcommon.TcpConnect(fileCopyData.ToHostIp, fileCopyData.ToHostPort, clientcommon.Timeout); err != nil {
		return fmt.Errorf("dest host connect failed: %w", err)
	}
	return nil
}

func sendFcopyRequest(fcopyData common.IconFCopyData, srcHostConn *common.NetConnection, destHostConn *common.NetConnection, srcHostJazVersion int, destHostJazVersion int) (int, error) {
	var err error
	var tcpEventData common.EventData
	var tcpRequestMessageData common.TCPMessage
	var tcpResponseMessageData common.TCPMessage
	var tcpJobRunData common.JobRunRequestData
	var responseData common.ResponseData

	tcpJobRunData.Type = common.AgentJobTypePutFile

	if err := clientcommon.PrepareJobRunRequest(iconExecData, &tcpJobRunData); err != nil {
		return common.JA_JOBEXEC_FAIL, err
	}

	tcpRequestMessageData.Kind = common.KindFileCopy
	tcpRequestMessageData.Version = 1.0
	tcpRequestMessageData.ServerID = fcopyData.ServerID
	tcpRequestMessageData.Hostname = fcopyData.ToHostName
	tcpRequestMessageData.Data = tcpJobRunData

	//send put request
	if destHostJazVersion == 1 {
		if err = clientcommon.Jaz1SendData(destHostConn, tcpRequestMessageData, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host initial request send failed : %w", err)
		}
	} else {
		// prepare event data
		tcpEventData.Event.UniqueKey = common.GetUniqueKey(common.IconExecManagerProcess)
		tcpEventData.NextProcess.Name = common.AgentManagerProcess
		tcpEventData.TCPMessage = &tcpRequestMessageData
		tcpEventData.NextProcess.Data = iconExecData
		tcpEventData.Event.Name = common.EventAgentJobRun
		if err = clientcommon.Jaz2SendData(destHostConn, tcpEventData, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host initial request send failed : %w", err)
		}
	}

	tcpJobRunData.Type = common.AgentJobTypeGetFile
	tcpRequestMessageData.Hostname = fcopyData.FromHostName
	tcpRequestMessageData.Data = tcpJobRunData

	//send get request
	if srcHostJazVersion == 1 {
		if err = clientcommon.Jaz1SendData(srcHostConn, tcpRequestMessageData, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("source host initial request send failed : %w", err)
		}
	} else {
		tcpEventData.Event.UniqueKey = common.GetUniqueKey(common.IconExecManagerProcess)
		tcpEventData.NextProcess.Name = common.AgentManagerProcess
		tcpEventData.TCPMessage = &tcpRequestMessageData
		tcpEventData.Event.Name = common.EventAgentJobRun
		tcpEventData.NextProcess.Data = iconExecData

		//send get request
		if err = clientcommon.Jaz2SendData(srcHostConn, tcpEventData, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_SUCCEED, fmt.Errorf("source host initial request send failed : %w", err)
		}

	}

	//receive initial response from dest host
	if destHostJazVersion == 1 {
		if tcpResponseMessageData, err = clientcommon.Jaz1ReceiveData(destHostConn, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host initial response receive failed : %w", err)
		}

		if (tcpResponseMessageData == common.TCPMessage{}) {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("response data from Tcp message is empt [ Data: %v ]", tcpResponseMessageData)
		}

		if tcpResponseMessageData.Data == nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("response data from Tcp message's Data is empt [ Data: %v ]", tcpResponseMessageData)
		}

		if err = utils.Convert(tcpResponseMessageData.Data, &responseData); err != nil {
			return common.JA_JOBRESULT_FAIL, err
		}
	} else {
		if tcpEventData, err = clientcommon.Jaz2ReceiveData(destHostConn, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_SUCCEED, fmt.Errorf("destination host initial response receives failed : %w", err)
		}

		if tcpEventData.TCPMessage == nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("response data from Tcp message is empt [ Data: %v ]", tcpEventData)
		}

		if tcpEventData.TCPMessage.Data == nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("response data from Tcp message's Data is empt [ Data: %v ]", tcpEventData)
		}

		if err = utils.Convert(tcpEventData.TCPMessage.Data, &responseData); err != nil {
			return common.JA_JOBRESULT_SUCCEED, err
		}
	}

	if responseData.Result != 0 {
		if destHostJazVersion == 1 {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host initial request processing failed : %s", *responseData.Message)
		}
		return common.JA_JOBRESULT_SUCCEED, nil
	}

	//copy file data
	if err := copyData(srcHostConn, destHostConn); err != nil {
		if destHostJazVersion == 1 {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("file copy failed: %w", err)
		}
		return common.JA_JOBRESULT_SUCCEED, nil
	}

	//receive final response from dest host
	if destHostJazVersion == 1 {
		if tcpResponseMessageData, err = clientcommon.Jaz1ReceiveData(destHostConn, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host final response receive failed : %w", err)
		}

		if err = utils.Convert(tcpResponseMessageData.Data, &responseData); err != nil {
			return common.JA_JOBRESULT_FAIL, err
		}
	} else {
		if tcpEventData, err = clientcommon.Jaz2ReceiveData(destHostConn, uint(fcopyData.FcopyTimeout)); err != nil {
			return common.JA_JOBRESULT_SUCCEED, nil
		}

		if err = utils.Convert(tcpEventData.TCPMessage.Data, &responseData); err != nil {
			return common.JA_JOBRESULT_SUCCEED, nil
		}
	}

	if responseData.Result != 0 {
		if destHostJazVersion == 1 {
			return common.JA_JOBRESULT_FAIL, fmt.Errorf("destination host final response receive failed : %s", *responseData.Message)
		}
	}

	return common.JA_JOBRESULT_SUCCEED, nil
}

func copyData(srcHostConn, destHostConn *common.NetConnection) error {
	const bufferSize = 32 * 1024
	buf := make([]byte, bufferSize)
	count := 1
	for {
		// set fcopy read timeout
		if err := srcHostConn.SetReadDeadline(time.Now().Add(time.Duration(fileCopyData.FcopyTimeout) * time.Second)); err != nil {
			return fmt.Errorf("file copy read timeout set error: %w", err)
		}

		nbytes, err := srcHostConn.Read(buf)
		if err != nil {
			if err == io.EOF {
				break
			}
			return fmt.Errorf("file copy read error: %w", err)
		}

		if nbytes == 0 {
			break
		}

		written := 0
		for written < nbytes {
			// set fcopy write timeout
			if err := destHostConn.SetWriteDeadline(time.Now().Add(time.Duration(fileCopyData.FcopyTimeout) * time.Second)); err != nil {
				return fmt.Errorf("file copy write timeout set error %w", err)
			}

			mbytes, err := destHostConn.Write(buf[written:nbytes])
			if err != nil {
				return fmt.Errorf("file copy write error: %w", err)
			}
			written += mbytes
		}
		count++
	}
	return nil
}

func main() {
	var srcHostJazVersion, destHostJazVersion int
	var err error
	var destHostConn, srcHostConn *common.NetConnection
	var innerJobnetMainId, innerJobId uint64
	var runCount, methodFlag int
	var flag bool

	//catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			//output stacktrace
			fmt.Fprintf(os.Stderr, "[FcopyIconClient] 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, "[FcopyIconClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

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

	clientcommon.ParseArgs()

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

	// Check necessary data
	innerJobnetMainId = iconExecData.RunJobData.InnerJobnetMainID
	if innerJobnetMainId <= 0 {
		err = fmt.Errorf("invalid inner_jobnet_main_id")
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}
	innerJobId = iconExecData.RunJobData.InnerJobID
	if innerJobId <= 0 {
		err = fmt.Errorf("invalid inner_job_id")
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}
	runCount = iconExecData.RunJobData.RunCount
	if runCount < 0 {
		err = fmt.Errorf("invalid run_count")
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}
	methodFlag = int(iconExecData.RunJobData.MethodFlag)
	if methodFlag < 0 {
		err = fmt.Errorf("invalid method_flag")
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

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

	// get fcopy icon data
	if err = utils.Convert(iconExecData.RunJobData.Data, &fileCopyData); err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	//check jaz version
	if srcHostJazVersion, _, err = clientcommon.CheckJazVersion(fileCopyData.FromHostIp, fileCopyData.FromHostPort, fileCopyData.FromHostName, fileCopyData.ServerID); err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	if destHostJazVersion, _, err = clientcommon.CheckJazVersion(fileCopyData.ToHostIp, fileCopyData.ToHostPort, fileCopyData.ToHostName, fileCopyData.ServerID); err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	fileCopyData.IsJaz1 = destHostJazVersion == 1

	// write jaz version in data file
	if err := clientcommon.WriteStructToFD3(fileCopyData); err != nil {
		fmt.Fprintln(os.Stderr, "[FcopyClient] Failed to write last status to fd3:", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	//connect to hosts
	if err = connectHosts(&srcHostConn, &destHostConn); err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	// create run event
	eventData = clientcommon.IconRunDataPrep(string(clientcommon.IconClientFCopy), iconExecData)

	err = event.CreateNextEvent(eventData, iconExecData.RunJobData.InnerJobnetID, iconExecData.RunJobnetData.JobnetID, iconExecData.RunJobData.InnerJobID)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBRESULT_PRE_EXEC_FAIL)
	}

	runStatusChangeFlag = true

	// flag file creation
	err = event.CreateRunCountFile(clientcommon.RunCountFolderPath, innerJobnetMainId, innerJobId, runCount)
	if err != nil {
		err = fmt.Errorf("failed to create flag file : %w", err)
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBRESULT_FAIL)
	}

	// search and re-assign env variable
	if err := clientcommon.SetBeforeVariable(iconExecData); err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	envVars, err := clientcommon.ResolveEnvVars(iconExecData)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[FcopyClient] %v", err)
		os.Exit(common.JA_JOBEXEC_FAIL)
	}

	fileCopyData.FromDirectory = envVars[fileCopyData.FromDirectory]
	fileCopyData.ToDirectory = envVars[fileCopyData.ToDirectory]
	fileCopyData.FromFileName = envVars[fileCopyData.FromFileName]

	//send request to hosts
	exitCode, err := sendFcopyRequest(fileCopyData, srcHostConn, destHostConn, srcHostJazVersion, destHostJazVersion)

	if srcHostConn != nil {
		srcHostConn.Close()
	}
	if destHostConn != nil {
		destHostConn.Close()
	}

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