/*
** 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 (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"

	"jobarranger2/src/jobarg_agent/managers/agent_manager/agentutils"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/config_reader/agent"
	"jobarranger2/src/libs/golibs/config_reader/conf"
	"jobarranger2/src/libs/golibs/logger/logger"
	"jobarranger2/src/libs/golibs/utils"
)

// Global variables
const (
	JA_FILE_PATH_LEN = 260 // according to C length
)

func main() {
	fn := "fcopy-client:main"
	var (
		agentData common.AgentDataW
		tcpData   common.JobRunRequestData
	)
	configFilePath := flag.String("config-file-path", "", "Full path to the jaz agent config")

	flag.Parse()

	if *configFilePath == "" {
		fmt.Fprintf(os.Stderr, "Error: -config-file-path is required [pid=%d]\n", os.Getpid())
		os.Exit(1)
	}

	// Load config
	if err := conf.Load(*configFilePath, &agent.Options); err != nil {
		fmt.Fprintf(os.Stderr, "In %s(), failed to load config file [pid=%d]: %v\n", fn, os.Getpid(), err)
		os.Exit(1)
	}

	// Init logger
	if err := logger.InitLogger(
		agent.Options.JaLogFile,
		agent.Options.JaLogMessageFile,
		agent.Options.LogType,
		agent.Options.LogFileSize*1024*1024,
		agent.Options.DebugLevel,
		logger.TargetTypeAgent,
	); err != nil {
		fmt.Fprintf(os.Stderr, "In %s(), failed to initialize logger [pid=%d]: %v", fn, os.Getpid(), err)
		os.Exit(1)
	}

	//catch runtime panic errors
	defer func() {
		if r := recover(); r != nil {
			//output stacktrace
			logger.WriteLog("JAAGENTFCOPY100001", string(debug.Stack()))
			os.Exit(1)
		}
	}()

	logger.WriteLog("JAAGENTFCOPY400012", fn)

	serverIPsPath := filepath.Join(agent.Options.TmpDir, common.AgentManagerFolder, "serverIPs", "serverIPs.json")
	common.LockFilePath = filepath.Join(agent.Options.TmpDir, "lock_file.lock")

	// Read std in data
	agentDataBytes, err := io.ReadAll(os.Stdin)
	if err != nil {
		logger.WriteLog("JAAGENTFCOPY200011", fn, err)
		os.Exit(1)
	}

	if err = json.Unmarshal(agentDataBytes, &agentData); err != nil {
		logger.WriteLog("JAAGENTFCOPY200012", fn, string(agentDataBytes), err)
		os.Exit(1)
	}

	var eventData common.EventData
	eventData, err = agentutils.UnmarshalAs[common.EventData](agentData.EventData)
	if err != nil {
		logger.WriteLog("JAAGENTFCOPY200013", fn, string(agentData.EventData), err)
		os.Exit(1)
	}

	netConn, err := agentutils.RecreateSocket(agentData.SocketFilePath)
	if err != nil {
		logger.WriteLog("JAAGENTFCOPY200014", fn, agentData.SocketFilePath, err)
		os.Exit(1)
	}

	defer func() {
		agentutils.CloseWrite(netConn)
		netConn.Close()
	}()

	eventData.TCPMessage.Kind = common.KindFileCopyRes

	// Get TCP Data
	dataBytes, err := json.Marshal(eventData.TCPMessage.Data)
	if err != nil {
		logger.WriteLog("JAAGENTFCOPY200015", fn, err)
		os.Exit(1)
	}

	if err := json.Unmarshal(dataBytes, &tcpData); err != nil {
		logger.WriteLog("JAAGENTFCOPY200016", fn, string(dataBytes), err)
		os.Exit(1)
	}

	var errMsg string
	var result int

	switch tcpData.Type {
	case common.AgentJobTypePutFile:
		if err := doPutFile(&tcpData, &eventData, netConn); err != nil {
			logger.WriteLog("JAAGENTFCOPY200019", fn, tcpData.Type, tcpData.JobID, err)
			errMsg = err.Error()
			result = common.JA_RESPONSE_FAIL
			eventData.TCPMessage.Data = common.ResponseData{
				Result:  result,
				Message: &errMsg,
			}

			if err := agentutils.SendToServer(&eventData, netConn); err != nil {
				logger.WriteLog("JAAGENTMNG200009", fn, tcpData.JobID, err)
				return
			}

		} else {
			result = common.JA_RESPONSE_SUCCEED
			eventData.TCPMessage.Data = common.ResponseData{
				Result: result,
			}
			if err := agentutils.SendToServer(&eventData, netConn); err != nil {
				logger.WriteLog("JAAGENTMNG200009", fn, tcpData.JobID, err)
				return
			}
		}

		// Send to trapper if server is JAZ2
		if *eventData.TCPMessage.JazVersion == 2 {
			eventData.TCPMessage.Data = common.JobResultData{
				JobRunRequestData: tcpData,
				Result:            result,
				Message:           errMsg,
			}
			eventData.Event.UniqueKey = common.GetUniqueKey(common.AgentManagerProcess)

			conn, err := agentutils.ConnectServer(eventData.TCPMessage.ServerID, serverIPsPath)
			if err != nil {
				logger.WriteLog("JAAGENTFCOPY200003", fn, err)
				return
			}

			if err := agentutils.SendToServer(&eventData, conn); err != nil {
				logger.WriteLog("JAAGENTMNG200009", fn, tcpData.JobID, err)
				return
			}

			var response map[string]any
			conn.SetReceiveTimeout(10)
			if response, err = conn.Receive(); err != nil {
				logger.WriteLog("JAAGENTFCOPY200005", fn, tcpData.JobID, err)
				return
			}

			logger.WriteLog("JAAGENTFCOPY400015", fn, tcpData.JobID, utils.ToReadableString(response))
		}

	case common.AgentJobTypeGetFile:
		if err := doGetFile(tcpData, *netConn); err != nil {
			logger.WriteLog("JAAGENTFCOPY200019", fn, tcpData.Type, tcpData.JobID, err)
			errMsg := err.Error()
			result = common.JA_RESPONSE_FAIL
			if err := agentutils.SendString(netConn, errMsg); err != nil {
				logger.WriteLog("JAAGENTFCOPY200006", fn, errMsg, err)
			}

			eventData.TCPMessage.Data = common.ResponseData{
				Result:  result,
				Message: &errMsg,
			}

		} else {
			result = common.JA_RESPONSE_SUCCEED
			eventData.TCPMessage.Data = common.ResponseData{
				Result: result,
			}
		}

		// Send to trapper if server is JAZ2
		if *eventData.TCPMessage.JazVersion == 2 {
			eventData.TCPMessage.Data = common.JobResultData{
				JobRunRequestData: tcpData,
				Result:            result,
				Message:           errMsg,
			}
			eventData.Event.UniqueKey = common.GetUniqueKey(common.AgentManagerProcess)

			conn, err := agentutils.ConnectServer(eventData.TCPMessage.ServerID, serverIPsPath)
			if err != nil {
				logger.WriteLog("JAAGENTFCOPY200003", fn, tcpData.JobID, err)
				return
			}

			if err := agentutils.SendToServer(&eventData, conn); err != nil {
				logger.WriteLog("JAAGENTMNG200009", fn, tcpData.JobID, err)
				return
			}

			var response map[string]any
			conn.SetReceiveTimeout(10)
			if response, err = conn.Receive(); err != nil {
				logger.WriteLog("JAAGENTFCOPY200005", fn, tcpData.JobID, err)
				return
			}

			logger.WriteLog("JAAGENTFCOPY400015", fn, tcpData.JobID, utils.ToReadableString(response))
		}

	default:
		logger.WriteLog("JAAGENTFCOPY200007", fn, tcpData.JobID, tcpData.Type)
		return
	}

	logger.WriteLog("JAAGENTFCOPY000001", fn, tcpData.JobID, tcpData.Type)

}

func doPutFile(tcpData *common.JobRunRequestData, eventData *common.EventData, netConn *common.NetConnection) error {

	funcName := "doPutFile"
	logger.WriteLog("JAAGENTFCOPY400013", funcName, tcpData.Type, tcpData.JobID)

	argument, ok := tcpData.Argument.(map[string]any)
	if !ok {
		return fmt.Errorf("invalid argument: %v", tcpData.Argument)
	}

	logger.WriteLog("JAAGENTFCOPY400001", funcName, argument, tcpData.JobID)

	utf8ToDir, ok := argument["todir"].(string)
	if !ok {
		return fmt.Errorf("cannot get todir from argument: %v", argument)
	}

	val, ok := argument["overwrite"]
	if !ok {
		return fmt.Errorf("overwrite not found in argument: %v", argument)
	}

	overwriteInt, err := agentutils.ParseAnyToInt(val)
	if err != nil {
		return fmt.Errorf("overwrite flag conversion failed: %v", err)
	}

	overwrite := overwriteInt == 1

	// Encode dir path
	encodedToDir, err := agentutils.EncodeString(utf8ToDir, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("failed to encode dest dir: %v, error: %v", argument, err)
	}

	// Check if toDir exists
	if err := agentutils.CheckDir(encodedToDir); err != nil {
		return fmt.Errorf("directory check failed '%s': %v", encodedToDir, err)
	}
	logger.WriteLog("JAAGENTFCOPY400002", funcName, encodedToDir)

	// Directory check success
	eventData.TCPMessage.Data = common.ResponseData{
		Result: common.JA_RESPONSE_SUCCEED,
	}

	if err := agentutils.SendToServer(eventData, netConn); err != nil {
		return fmt.Errorf("directory check success send failed: %v", err)
	}

	netConn.SetReceiveTimeout(int64(agent.Options.JaFcopyTimeout))
	var fileCount int32

	// Step 1: Read file count (4 bytes)
	if err := binary.Read(netConn, binary.LittleEndian, &fileCount); err != nil {
		return fmt.Errorf("failed to read file count: %w", err)
	}
	if fileCount < 1 {
		logger.WriteLog("JAAGENTFCOPY400011", funcName, fileCount)
		msg, err := agentutils.ReceiveString(netConn)
		if err != nil {
			logger.WriteLog("JAAGENTFCOPY200018", funcName, err)
		}
		return fmt.Errorf("%s", msg)
	}

	for i := int32(0); i < fileCount; i++ {
		// Step 2: Read fcopyflag (1 byte)
		flagBuf := make([]byte, 1)
		if _, err := io.ReadFull(netConn, flagBuf); err != nil {
			return fmt.Errorf("failed to read fcopyflag: %w", err)
		}
		fcopyflag := flagBuf[0]

		if fcopyflag != common.JA_FCOPY_FLAG_SUCCEED { // Assume 'S' == JA_FCOPY_FLAG_SUCCEED
			return fmt.Errorf("fcopyflag not successful: %c", fcopyflag)
		}

		// Step 3: Read filename
		filenameBuf := make([]byte, JA_FILE_PATH_LEN) // based on C size
		if _, err := io.ReadFull(netConn, filenameBuf); err != nil {
			return fmt.Errorf("failed to read filename: %w", err)
		}
		utf8Filename := strings.TrimRight(string(filenameBuf), "\x00")

		// Encode filename
		encodedFilename, err := agentutils.EncodeString(utf8Filename, agent.Options.Locale)
		if err != nil {
			return fmt.Errorf("failed to encode received filename: %v, error: %v ", encodedFilename, err)
		}

		fullPath := filepath.Join(encodedToDir, encodedFilename)

		if !overwrite {
			if _, err := os.Stat(fullPath); err == nil {
				return fmt.Errorf("%s cannot be overwritten. Please check the overwrite", fullPath)
			}
		}

		// Step 4: Extract TAR from conn to destDir
		if err := agentutils.ExtractTarToDir(netConn, encodedToDir, overwrite); err != nil {
			return fmt.Errorf("failed to extract TAR stream: %w", err)
		}

		// Step 5: Read checksum size (int32)
		var chksumSize int32
		if err := binary.Read(netConn, binary.LittleEndian, &chksumSize); err != nil {
			return fmt.Errorf("failed to read checksum size: %w", err)
		}

		// Step 6: Read checksum data
		chksumBuf := make([]byte, chksumSize)
		if _, err := io.ReadFull(netConn, chksumBuf); err != nil {
			return fmt.Errorf("failed to read checksum: %w", err)
		}
		chksum := string(chksumBuf)
		logger.WriteLog("JAAGENTFCOPY400003", funcName, chksum)

		// Verify checksum
		checksumMap, err := UnmarshalRawBytes(chksumBuf)
		if err != nil {
			return fmt.Errorf("failed to unmarshal checksum map: %v", err)
		}

		for rawFilenameStr, expectedChecksum := range checksumMap {
			// Trim spaces
			cleanFilename := strings.Trim(rawFilenameStr, " \"")

			// Encode filename
			encodedFilename, err := agentutils.EncodeString(cleanFilename, agent.Options.Locale)
			if err != nil {
				return fmt.Errorf("failed to encode received checksum filename: %v, error: %v ", encodedFilename, err)
			}

			fullPath := filepath.Join(encodedToDir, encodedFilename)
			actualChecksum, err := agentutils.ComputeChecksum(fullPath)
			if err != nil {
				return fmt.Errorf("cannot compute checksum for %s: %w", fullPath, err)
			}

			// Trim the expected checksum
			expectedChecksum = strings.Trim(expectedChecksum, " \"")

			if actualChecksum != expectedChecksum {
				return fmt.Errorf("checksum mismatch for %s: expected %s, got %s",
					cleanFilename, expectedChecksum, actualChecksum)
			}
		}

	}

	getFileRetStr, err := agentutils.ReceiveString(netConn)
	if err != nil {
		return fmt.Errorf("failed to receive ret from getfile: %v", err)
	}

	getFileRet, err := strconv.Atoi(getFileRetStr)
	if err != nil {
		return fmt.Errorf("getfile ret conversion failed [%s]: %w", getFileRetStr, err)
	}

	logger.WriteLog("JAAGENTFCOPY400004", funcName, getFileRet)

	if getFileRet != common.JA_RESPONSE_SUCCEED {
		return fmt.Errorf("getfile ret is not successful: %d", getFileRet)
	}

	return nil

}

func doGetFile(tcpData common.JobRunRequestData, netConn common.NetConnection) error {
	funcName := "doGetFile"
	logger.WriteLog("JAAGENTFCOPY400013", funcName, tcpData.Type, tcpData.JobID)
	// var responseData common.ResponseData
	fcopyFlag := common.JA_FCOPY_FLAG_FAIL

	argument, ok := tcpData.Argument.(map[string]any)
	if !ok {
		return fmt.Errorf("malformed argument: %v", tcpData.Argument)
	}

	logger.WriteLog("JAAGENTFCOPY400001", funcName, argument, tcpData.JobID)

	// parse values from argument
	utf8FromDir, ok := argument["fromdir"].(string)
	if !ok {
		return fmt.Errorf("cannot get fromdir from argument: %v", argument)
	}

	utf8FileName, ok := argument["filename"].(string)
	if !ok {
		return fmt.Errorf("cannot get filename from argument: %v", argument)
	}

	// Encode dir path
	encodeFromDir, err := agentutils.EncodeString(utf8FromDir, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("failed to encode from dir: %v, errors: %v", argument, err)
	}

	// Encode file name
	encodeFileName, err := agentutils.EncodeString(utf8FileName, agent.Options.Locale)
	if err != nil {
		return fmt.Errorf("failed to encode source filename: %v, error: %v", argument, err)
	}

	// Get matched filepaths
	files, err := agentutils.FindMatchingFiles(encodeFromDir, encodeFileName)
	if err != nil {
		if err := agentutils.SendFileCount(&netConn, int32(0)); err != nil {
			logger.WriteLog("JAAGENTFCOPY200020", funcName, 0, tcpData.JobID, err)
		}
		return fmt.Errorf("failed to find matching files: %v", err)
	}

	// No matched file
	if len(files) <= 0 {
		if err := agentutils.SendFileCount(&netConn, int32(0)); err != nil {
			logger.WriteLog("JAAGENTFCOPY200020", funcName, 0, tcpData.JobID, err)
		}
		return fmt.Errorf("no file match the regex. [%s]", encodeFileName)
	}

	netConn.SetSendTimeout(int64(agent.Options.JaFcopyTimeout))
	// Send the file count
	fileCount := int32(len(files))
	if err := agentutils.SendFileCount(&netConn, fileCount); err != nil {
		return fmt.Errorf("failed to send file count [%d]: %v", fileCount, err)
	}

	for _, targetFilePath := range files {
		// Get checksum json
		checksumMap := map[string]string{}
		rawName := filepath.Base(targetFilePath)

		// Convert file names to UTF-8
		targetFileName, err := agentutils.DecodeString(rawName, agent.Options.Locale)
		if err != nil {
			return fmt.Errorf("failed to decode source filename: %w", err)
		}

		checksum, err := agentutils.ComputeChecksum(targetFilePath)
		if err != nil {
			return fmt.Errorf("compute checksum failed: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400005", funcName, targetFilePath, tcpData.JobID, checksum)
		checksumMap[targetFileName] = checksum

		// Check checksum size
		if len(checksum) < 5 {
			fcopyFlag = common.JA_FCOPY_FLAG_NOFILE
			return fmt.Errorf("checksum size failed for '%s'", targetFilePath)
		}

		fcopyFlag = common.JA_FCOPY_FLAG_SUCCEED

		// Send fcopy flag
		if err := binary.Write(netConn, binary.LittleEndian, byte(fcopyFlag)); err != nil {
			return fmt.Errorf("fcopy flag send error: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400006", funcName, fcopyFlag, tcpData.JobID)
		// Send file name
		buf := make([]byte, JA_FILE_PATH_LEN)
		copy(buf, targetFileName) // remaining bytes are zero-initialized

		if _, err := netConn.Write(buf); err != nil {
			return fmt.Errorf("target filename send error: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400007", funcName, targetFileName, tcpData.JobID)

		if err := agentutils.TarFilesToSocket(netConn, targetFilePath); err != nil {
			return fmt.Errorf("cannot send the tar file: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400008", funcName, targetFilePath, tcpData.JobID)

		// Checksum map to string
		chksumBytes, err := MarshalRawBytes(checksumMap)
		if err != nil {
			log.Fatalf("JSON marshal failed: %v", err)
		}
		chksum := string(chksumBytes)

		// Send checksum size and checksum
		chksumSize := int32(len(chksum))
		if err := binary.Write(netConn, binary.LittleEndian, chksumSize); err != nil {
			return fmt.Errorf("checksum size send error: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400009", funcName, chksumSize, tcpData.JobID)

		if _, err := netConn.Write([]byte(chksum)); err != nil {
			return fmt.Errorf("checksum send error: %v", err)
		}

		logger.WriteLog("JAAGENTFCOPY400010", funcName, chksum, tcpData.JobID)
	}

	// Send success message
	retStr := fmt.Sprintf("%d", 0) // convert int to string

	if err := agentutils.SendString(&netConn, retStr); err != nil {
		return fmt.Errorf("ret str send error: %v", err)
	}

	logger.WriteLog("JAAGENTFCOPY400014", funcName, retStr, tcpData.JobID)

	return nil
}

func MarshalRawBytes(checksumMap map[string]string) ([]byte, error) {
	var buf bytes.Buffer
	buf.WriteByte('{')

	first := true
	for k, v := range checksumMap {
		if !first {
			buf.WriteByte(',')
		}
		first = false

		// Convert key to raw string literal for JSON
		key := string(k) // raw bytes as string

		// Escape quotes and backslashes in key
		key = strings.ReplaceAll(key, "\\", "\\\\")
		key = strings.ReplaceAll(key, "\"", "\\\"")

		// Escape value normally for JSON string
		val := strings.ReplaceAll(v, "\\", "\\\\")
		val = strings.ReplaceAll(val, "\"", "\\\"")

		buf.WriteString(fmt.Sprintf("\"%s\":\"%s\"", key, val))
	}

	buf.WriteByte('}')
	return buf.Bytes(), nil
}

func UnmarshalRawBytes(data []byte) (map[string]string, error) {
	result := make(map[string]string)

	// Simple parser assuming old C sender: {"<rawbytes>":"checksum"}
	// Split into key:value pairs manually
	data = bytes.TrimSpace(data)
	if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
		return nil, fmt.Errorf("invalid JSON object")
	}
	data = data[1 : len(data)-1]

	items := bytes.Split(data, []byte{','})
	for _, item := range items {
		kv := bytes.SplitN(item, []byte{':'}, 2)
		if len(kv) != 2 {
			continue
		}
		key := bytes.Trim(kv[0], "\"")             // keep raw bytes
		value := strings.Trim(string(kv[1]), "\"") // checksum is UTF-8
		result[string(key)] = value                // store raw bytes as string
	}

	return result, nil
}
