/* Copyright 2023 m-privacy GmbH.
 *
 * This 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 software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */


#ifndef WIN32

#include <iostream>
#include <nlohmann/json.hpp>
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <curl/curl.h>
#include <libgen.h>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <archive.h>
#include <archive_entry.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <magic.h>
#include <stdbool.h>
#include <signal.h>
#include <ftw.h>

#if !defined(WIN32) && !defined(WIN64)
#include <syslog.h>
#endif

#include <rfb/LogWriter.h>

#include <tgpro_environment.h>
#include <mp_utils.h>

#include "SAutotransferVaithex.h"
#include "SAutotransferMime.h"
#include <magic.h>
#define MAGIC_ALLOWED 0
#define MAGIC_NOT_ALLOWED 1
#define MAGIC_FAILED -1

using namespace std;
using json = nlohmann::json;

static rfb::LogWriter vlog("SAutotransferVaithex");

rfb::StringParameter vaithexIp("vaithexIp","IP address of the VAITHEX server to clean files and (auto)-download them to the client","");

rfb::StringParameter vaithexPort("vaithexPort","Port of the VAITHEX server to clean files and (auto)-download them to the client","443");

rfb::IntParameter vaithexTimeout("vaithexTimeout", "VAITHEX timeout in seconds (min 60, max 3600)", 300, 60, 3600);

rfb::StringParameter vaithexScanners ("vaithexScanners","List of scanners the VAITHEX server should use to process files","");  // Empty default. It will be populated dynamically

rfb::StringParameter vaithexSuffix ("vaithexSuffix", "Suffix appended to filenames of files normalized by VAITHEX", "");

static constexpr const char* CA_CERT_PATH = "/home/user/.customvaithexca/ca-certificates";

char* vaithexScannersAsJsonStr = NULL;

std::vector<std::string> getAvailableScannersFromVaithex();

// Function declarations
static int createTarWithSingleFile(const char* tarPath, const char* dir, const char* filename);
static int extractAll(const char* filename, const char* outputDir, char* lastExtractedFile);

// Callback function for nftw() to remove files and directories
static int removeCallback(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) {
	int rv = remove(fpath);
	if (rv) {
		vlog.error("Failed to remove '%s': %s", fpath, strerror(errno));
	} else {
		vlog.debug("Removed: %s", fpath);
	}
	return rv;
}

// Recursively remove a directory and all its contents
static int removeDirectory(const char* path) {
	// FTW_DEPTH: do depth-first search (process directory contents before the directory itself)
	return nftw(path, removeCallback, 64, FTW_DEPTH | FTW_PHYS);
}

static std::string getBasename(const char* path)
{
	if (!path) return "";

	const char* slash = strrchr(path, '/');
	return slash ? std::string(slash + 1) : std::string(path);
}

static std::string getDirname(const char* path)
{
	if (!path) return "";

	const char* slash = strrchr(path, '/');

	if (!slash) return ".";

	if (slash == path) return "/";

	return std::string(path, slash - path);
}

static std::string getSanitizedVaithexSuffix()
{
	std::string suffix = vaithexSuffix.getData();
	if (suffix.empty()) {
	    // Explicitely allow empty suffixx
		return suffix;
	}
	std::string sanitized;
	vlog.debug("%s: raw vaithexSuffix value from parameter: '%s'", __func__, suffix.c_str());
	sanitized.reserve(suffix.size());
	// In theory, this shouldn't be necessary, as it was sanitized with the config menu
	// but it doesn't hurt to double-check. The only tricky part could be to
	// make sure the same chars are allowed. If suffix != sanizized, we'll show an erro
	// in the VNC server log
	for (char c : suffix) {
		if (isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-') {
			sanitized += c;
		}
	}
	if (sanitized != suffix) {
		vlog.error("%s: Suffix had to be sanitized: %s -> %s", __func__, suffix.c_str(), sanitized.c_str());
	}
	if (sanitized.empty()) {
		vlog.error("%s: configured vaithexSuffix sanitizes to empty, falling back to default", __func__);
		sanitized = "___vaithexed";
	}
	return sanitized;
}

void cleanupTempDir(const char* dirPath) {
	if (!dirPath || strlen(dirPath) == 0) {
		vlog.error("%s: dirPath is NULL or empty", __func__);
		return;
	}
	// Only delete allowed directories
	if (strncmp(dirPath, "/home/tmpdir/tmp", 16) != 0 &&
		strncmp(dirPath, "/tmp/vaithex", 12) != 0) {
		vlog.error("%s: Refusing to delete directory outside allowed paths: %s", __func__, dirPath);
	return;
		}
		// Check if directory exists
		struct stat st;
		if (stat(dirPath, &st) != 0) {
			vlog.debug("%s: Directory does not exist (already cleaned?): %s", __func__, dirPath);
			return;
		}

		if (!S_ISDIR(st.st_mode)) {
			vlog.error("%s: Path exists but is not a directory: %s", __func__, dirPath);
			return;
		}

		vlog.debug("%s: Cleaning up temporary directory: %s", __func__, dirPath);

		int result = removeDirectory(dirPath);

		if (result != 0) {
			vlog.error("%s: Failed to remove temporary directory: %s", __func__, dirPath);
		} else {
			vlog.debug("%s: Successfully cleaned up: %s", __func__, dirPath);
		}
}

void cleanupTempTarFiles(const char* filename) {
	if (!filename || strlen(filename) == 0) {
		vlog.error("%s: filename is NULL or empty", __func__);
		return;
	}

	vlog.debug("%s: Cleaning up temporary tar files for: %s", __func__, filename);

	char tarFile[512];
	char resultTarFile[512];
	snprintf(tarFile, sizeof(tarFile), "/tmp/%s.tar", filename);
	snprintf(resultTarFile, sizeof(resultTarFile), "/tmp/%s-result.tar", filename);

	// Clean up both tar files individually
	for (int i = 0; i < 2; i++) {
		const char* fileToDelete = (i == 0) ? tarFile : resultTarFile;

		struct stat st;
		if (stat(fileToDelete, &st) == 0) {
			vlog.debug("%s: Removing tar file: %s (size: %ld bytes)",
					   __func__, fileToDelete, (long)st.st_size);

			if (remove(fileToDelete) == 0) {
				vlog.debug("%s: Successfully removed: %s", __func__, fileToDelete);
			} else {
				vlog.error("%s: Failed to remove %s: %s",
						   __func__, fileToDelete, strerror(errno));
			}
		} else {
			vlog.debug("%s: Tar file does not exist (already cleaned?): %s",
					   __func__, fileToDelete);
		}
	}
}

static std::string trim(const std::string& s) {
	size_t start = s.find_first_not_of(" \t\r\n");
	if (start == std::string::npos) {
		return "";
	}

	size_t end = s.find_last_not_of(" \t\r\n");
	return s.substr(start, end - start + 1);
}

[[deprecated("Not using anymore - it works, but slows down login unnecessarily")]]
std::vector<std::string> getAvailableScannersFromVaithex() {
	std::vector<std::string> availableScanners;

	char sysinfoCmd[1500];
	snprintf(sysinfoCmd, sizeof(sysinfoCmd),
			 "wget --ca-certificate=\"%s\" --quiet --output-document=- "
			 "'https://%s:%s/v1/sysinfo'",
		  CA_CERT_PATH, vaithexIp.getData(), vaithexPort.getData());

	vlog.debug("%s: Fetching available scanners: %s", __func__, sysinfoCmd);

	FILE* sysinfoStream = popen(sysinfoCmd, "r");
	if (!sysinfoStream) {
		vlog.error("%s: Failed to execute sysinfo command", __func__);
		return availableScanners;
	}

	char response[8192];
	size_t responseSize = fread(response, 1, sizeof(response) - 1, sysinfoStream);
	pclose(sysinfoStream);

	if (responseSize == 0) {
		vlog.error("%s: Empty response from sysinfo endpoint", __func__);
		return availableScanners;
	}
	response[responseSize] = '\0';

	vlog.debug("%s: Sysinfo response: %s", __func__, response);

	try {
		json sysinfoJson = json::parse(response);

		if (sysinfoJson.contains("scanners")) {
			for (json::iterator it = sysinfoJson["scanners"].begin();
				 it != sysinfoJson["scanners"].end(); ++it) {
				std::string scannerName = it.key();
			json scannerInfo = it.value();

			bool installed = scannerInfo.value("installed", false);
			bool available = scannerInfo.value("available", false);

			if (installed && available) {
				availableScanners.push_back(scannerName);
				vlog.debug("%s: Found available scanner: %s", __func__, scannerName.c_str());
			}
				 }
		}
	} catch (const json::parse_error& e) {
		vlog.error("%s: JSON parsing error: %s", __func__, e.what());
	}

	return availableScanners;
}

[[deprecated("Not using anymore - it works, but slows down login unnecessarily")]]
void initializeVaithexScanners() {
	// Fetch available scanners from the VAITHEX server
	std::vector<std::string> availableScanners = getAvailableScannersFromVaithex();

	if (!availableScanners.empty()) {
		// Display scanners returned from the VAITHEX server
		std::string serverScannersList;
		for (size_t i = 0; i < availableScanners.size(); i++) {
			if (i > 0) serverScannersList += ",";
			serverScannersList += availableScanners[i];
		}
		vlog.info("%s: Scanners available from VAITHEX server: %s",
				  __func__, serverScannersList.c_str());

		// Log the currently configured scanners (from user config)
		if (strlen(vaithexScanners.getData()) == 0) {
			vlog.info("%s: No scanners configured by user",
					  __func__);
		} else {
			vlog.info("%s: User-configured scanners: %s",
					  __func__, vaithexScanners.getData());
		}
	} else {
		vlog.error("%s: No scanners detected from the VAITHEX server", __func__);
	}
}

// Static flag to ensure cleanup runs only once per process
static bool g_loginCleanupDone = false;

SAutotransferVaithex::SAutotransferVaithex() {
	vlog.debug("%s: Setting timeout for processFile calls to %d seconds", __func__, (int) vaithexTimeout);
	setProcessFileTimeout((int) vaithexTimeout);
	const std::string effectiveUser = getEffectiveUser();
	setLogPathForEvents("/home/user/.vaithexlog/" + effectiveUser); // For mp-watch
	antivirusesEnabled = access(("/home/user/.vaithexavenabled/" + effectiveUser).c_str(), F_OK) == 0;
	vlog.debug("%s: VAITHEX virus detection activated: %s", __func__, antivirusesEnabled ? "yes" : "no");
	normalizationEnabled = access(("/home/user/.vaithexnormenabled/" + effectiveUser).c_str(), F_OK) == 0;
	vlog.debug("%s: VAITHEX normalization activated: %s", __func__, normalizationEnabled ? "yes" : "no");

	mimeChecker = new SAutotransferMime();
	// Initialize scanners from Vaithex if not already configured
	// initializeVaithexScanners();

	if (!g_loginCleanupDone) {
		vlog.info("%s: Running one-time login cleanup for user %s (PID: %d)",
				  __func__, getenv("USER"), getpid());

		// Clean up old /tmp/vaithex-tmp-* directories
		vlog.info("%s: Scanning for old /tmp/vaithex-tmp-* directories", __func__);

		DIR* tmpDir = opendir("/tmp");
		if (tmpDir) {
			struct dirent* entry;
			int cleanedCount = 0;

			while ((entry = readdir(tmpDir)) != NULL) {
				if (strncmp(entry->d_name, "vaithex-tmp-", 12) == 0) {
					char fullPath[512];
					snprintf(fullPath, sizeof(fullPath), "/tmp/%s", entry->d_name);
					// Extract PID from directory name
					pid_t dirPid = atoi(entry->d_name + 12);
					// Check if this is our current temp directory
					if (dirPid == getpid()) {
						vlog.debug("%s: Skipping current process temp dir: %s", __func__, fullPath);
						continue;
					}
					// Check if the process is still running
					if (kill(dirPid, 0) == 0) {
						vlog.debug("%s: Skipping %s - process %d is still running",
								   __func__, fullPath, dirPid);
					} else if (errno == ESRCH) {
						// Process doesn't exist, safe to delete
						vlog.info("%s: Deleting stale directory: %s (PID %d not running)",
								  __func__, fullPath, dirPid);
						if (removeDirectory(fullPath) == 0) {
							cleanedCount++;
						}
					}
				}
			}
			closedir(tmpDir);
			vlog.info("%s: Cleaned %d stale vaithex-tmp directories",
					  __func__, cleanedCount);
		} else {
			vlog.error("%s:  Failed to open /tmp directory: %s",
					   __func__, strerror(errno));
		}
		//  Clean up old session directories
		//vlog.info("%s: Cleaning old session directories", __func__);
		// Clean up old tar files
		vlog.info("%s: Cleaning old tar files from /tmp", __func__);

		DIR* tmpDirForTars = opendir("/tmp");
		if (tmpDirForTars) {
			struct dirent* entry;
			int removedCount = 0;
			time_t now = time(NULL);

			while ((entry = readdir(tmpDirForTars)) != NULL) {
				if (entry->d_type != DT_REG) continue;
				// Check if it's a tar file
				size_t len = strlen(entry->d_name);
				if (len < 4) continue;

				bool isTarFile = (strcmp(entry->d_name + len - 4, ".tar") == 0);
				bool isResultTar = (len > 11 && strcmp(entry->d_name + len - 11, "-result.tar") == 0);

				if (isTarFile || isResultTar) {
					char fullPath[512];
					snprintf(fullPath, sizeof(fullPath), "/tmp/%s", entry->d_name);

					struct stat st;
					if (stat(fullPath, &st) == 0) {
						// Only delete files older than 1 day
						double ageHours = difftime(now, st.st_mtime) / 3600.0;
						if (ageHours > 24.0) {
							vlog.debug("%s: Removing old tar file: %s (age: %.1f hours)",
									   __func__, fullPath, ageHours);
							if (remove(fullPath) == 0) {
								removedCount++;
							}
						}
					}
				}
			}
			closedir(tmpDirForTars);
			vlog.info("%s: [CLEANUP 3/3] Removed %d old tar files", __func__, removedCount);
		}

		vlog.info("%s: Login cleanup completed", __func__);
		g_loginCleanupDone = true;
	}
}

char* listFirstFilePathInDir(const char* dirName) {
	DIR* dir;
	struct dirent* entry;

	dir = opendir(dirName);

	if (dir == NULL) {
		vlog.error("%s: failed to open %s", __func__, dirName);
		return NULL;
	}

	char* firstFilePath = NULL;
	entry = readdir(dir);

	if (entry != NULL) {
		if (entry->d_type == DT_REG) {
			firstFilePath = (char*)malloc(strlen(dirName) + 1 + strlen(entry->d_name) + 1);
			if (firstFilePath != NULL) {
				sprintf(firstFilePath, "%s/%s", dirName, entry->d_name);
			}
		}
	}

	closedir(dir);
	return firstFilePath;
}

static const uint64_t LARGE_FILE_AV_SKIP_THRESHOLD_BYTES = 250ULL * 1024ULL * 1024ULL; // 250 MiB
static const uint64_t MAX_VAITHEX_FILE_BYTES = (78ULL * 1024ULL * 1024ULL * 1024ULL) / 10ULL; // 7.8 GiB safety limit, kept below 8 GiB to avoid boundary/tar overhead issues.

static bool shouldSkipScannerForLargeFile(const std::string& scanner)
{
	// These scanners are too sensitive with big files:
	// ikarus complains directly because of config; symantec can actually crash the whole machine.
	return scanner == "ikarus_scanserver" || scanner == "symantec";
}

json SAutotransferVaithex::getConfiguredVaithexScannersJson()
{
	json scannersArray = json::array();

	const char* scannersRaw = vaithexScanners.getData();
	if (!scannersRaw || strlen(scannersRaw) == 0) {
		vlog.info("%s: No scanners configured, returning empty scanner array", __func__);
		return scannersArray;
	}

	std::stringstream ss(scannersRaw);
	std::string scanner;

	while (std::getline(ss, scanner, ',')) {
		scanner = trim(scanner);

		if (scanner.empty()) {
			continue;
		}

		scannersArray.push_back(scanner);
		vlog.debug("%s: Added configured scanner: %s", __func__, scanner.c_str());
	}

	vlog.info(
		"%s: Configured scanners: %s",
		__func__,
		scannersArray.dump().c_str()
	);

	return scannersArray;
}

void SAutotransferVaithex::filterLargeFileUnsupportedScanners(json& scannersArray)
{
	json filteredScanners = json::array();

	for (const auto& scannerJson : scannersArray) {
		if (!scannerJson.is_string()) {
			continue;
		}

		std::string scanner = scannerJson.get<std::string>();

		if (shouldSkipScannerForLargeFile(scanner)) {
			vlog.info("%s: Removing scanner '%s' for large file", __func__, scanner.c_str());
			continue;
		}

		filteredScanners.push_back(scanner);
	}

	scannersArray = filteredScanners;
}

char* getVaithexScannersAsJsonStr() {
	if (!vaithexScannersAsJsonStr) {
		const char* scanners = vaithexScanners.getData();
		vlog.debug("%s: vaithexScanners parameter value: '%s'", __func__, scanners);

		// Check if scanners is empty or not configured
		if (strlen(scanners) == 0) {
			vlog.info("%s: No scanners configured, returning empty array", __func__);
			vaithexScannersAsJsonStr = strdup("[]");
		} else {
			// Parse the comma-separated scanner list
			char* input = strdup(scanners);
			vlog.debug("%s: Parsing scanner list: %s", __func__, input);

			// Build JSON array from configured scanners
			json scannersArray = json::array();
			char* token = strtok((char*) input, ",");

			while (token != NULL) {
				// Trim whitespace
				while (*token == ' ' || *token == '\t') token++;
				char* end = token + strlen(token) - 1;
				while (end > token && (*end == ' ' || *end == '\t')) end--;
				*(end + 1) = '\0';

				if (strlen(token) > 0) {
					scannersArray.push_back(token);
					vlog.debug("%s: Added scanner: %s", __func__, token);
				}

				token = strtok(NULL, ",");
			}

			free(input);
			vaithexScannersAsJsonStr = strdup(scannersArray.dump().c_str());
			vlog.info("%s: Using configured scanners: %s", __func__, vaithexScannersAsJsonStr);
		}
	}
	return vaithexScannersAsJsonStr;
}

string SAutotransferVaithex::getWatcherName() const {
	return "SAutotransferVaithex";
}

string SAutotransferVaithex::getDirToWatch() const
{
	return "/home/user/.transfer/" + getEffectiveUser() + "/vaithex";
}

void checkCACertificate(const char* caCertPath) {
	if (access(caCertPath, F_OK) != -1) {
		vlog.debug("%s: CA certificate file found at %s", __func__, caCertPath);
	} else {
		vlog.error("%s: CA certificate file not found at %s", __func__, caCertPath);
		exit(EXIT_FAILURE);
	}
}

bool SAutotransferVaithex::createJob(VaithexJob& job, uint64_t fileSizeBytes) {
	json jobsPostBodyJson;
	json scannersArray = json::array();

	if (antivirusesEnabled) {
		scannersArray = getConfiguredVaithexScannersJson();

		if (fileSizeBytes > LARGE_FILE_AV_SKIP_THRESHOLD_BYTES) {
			vlog.info(
				"%s: File is larger than %llu bytes; disabling ikarus and symantec for this job",
				__func__,
				static_cast<unsigned long long>(LARGE_FILE_AV_SKIP_THRESHOLD_BYTES)
			);

			filterLargeFileUnsupportedScanners(scannersArray);
		}
	}

	jobsPostBodyJson["normalize"] = normalizationEnabled;
	jobsPostBodyJson["scanners_enabled"] = scannersArray;
	jobsPostBodyJson["server_timeout_sec"] = 0;

	std::string jobsPostBody = jobsPostBodyJson.dump();

	checkCACertificate(CA_CERT_PATH);

	std::string url = std::string("https://") + vaithexIp.getData() + ":" + vaithexPort.getData() + "/v1/jobs";

	vlog.debug("%s: creating VAITHEX job at: %s", __func__, url.c_str());
	vlog.debug("%s: VAITHEX job config: %s", __func__, jobsPostBody.c_str());

	std::string response_buffer;
	CURLcode curlResult = curlPostStringToString(url, CA_CERT_PATH, NULL, jobsPostBody.c_str(), response_buffer);
	if (curlResult != CURLE_OK) {
		vlog.error("%s: curl error (%d): %s", __func__, (int)curlResult, curl_easy_strerror(curlResult));
		return false;
	}

	if (response_buffer.empty()) {
		vlog.error("%s: Empty response from the server", __func__);
		return false;
	}

	vlog.debug("Response from server: %s", response_buffer.c_str());

	try {
		json response_json = json::parse(response_buffer);

		if (response_json.contains("id") && response_json.contains("token")) {
			job.id = response_json["id"];
			job.token = response_json["token"];
			vlog.debug("%s: Job ID: %ld, Token: %s", __func__, job.id, job.token.c_str());
		} else {
			vlog.error("%s: JSON response is missing 'id' or 'token' fields", __func__);
			return false;
		}
	} catch (const json::parse_error& e) {
		vlog.error("%s: JSON parsing error: %s", __func__, e.what());
		return false;
	}

	return true;
}

// Returns bool to indicate success/failure
bool SAutotransferVaithex::uploadPayload(const VaithexJob& job, const char* filename, const char* dir) {
	vlog.debug("%s: Uploading payload for filename: %s", __func__, filename);

	char uploadUrl[600];
	snprintf(uploadUrl, sizeof(uploadUrl), "https://%s:%s/v1/jobs/%ld/payload", vaithexIp.getData(), vaithexPort.getData(), job.id);

	// Create the tar file with proper quoting for spaces
	char tarFile[1024];
	snprintf(tarFile, sizeof(tarFile), "/tmp/%s.tar", filename);

	vlog.info("%s: Creating tar file: %s", __func__, tarFile);

	if (createTarWithSingleFile(tarFile, dir, filename) != 0) {
		vlog.error("%s: Failed to create tar file for %s", __func__, filename);
		return false;
	}

	// Upload
	struct curl_slist* headers = NULL;
	std::string tokenHeader = std::string("X-Waas-Token: ") + job.token;
	headers = curl_slist_append(headers, tokenHeader.c_str());
	if (!headers) {
		vlog.error("%s: Failed to create curl header list", __func__);
		return false;
	}

	std::string uploadResponse;
	CURLcode curlResult = curlPostFileToString(uploadUrl, CA_CERT_PATH, NULL, tarFile, uploadResponse, headers);
	curl_slist_free_all(headers);

	if (curlResult == CURLE_OK) {
		vlog.debug("%s: Payload uploaded for filename: %s", __func__, filename);
	} else {
		vlog.error("%s: Payload upload failed for filename: %s. curl error (%d): %s", __func__, filename, (int)curlResult, curl_easy_strerror(curlResult));
		return false;
	}
	vlog.debug("Uploaded tar file name: %s.tar", filename);

	return true;
}

// Returns bool to indicate success/failure
bool SAutotransferVaithex::fetchMetadata(const VaithexJob& job, json& metadataJson) {
	char metadata_url[600];
	snprintf(metadata_url, sizeof(metadata_url), "https://%s:%s/v1/jobs/%ld", vaithexIp.getData(), vaithexPort.getData(), job.id);

	vlog.debug("%s: Fetching metadata from: %s", __func__, metadata_url);

	struct curl_slist* headers = NULL;
	std::string tokenHeader = std::string("X-Waas-Token: ") + job.token;
	headers = curl_slist_append(headers, tokenHeader.c_str());
	if (!headers) {
		vlog.error("%s: Failed to create curl header list", __func__);
		return false;
	}

	std::string metadata_response;
	CURLcode curlResult = curlGetToString(metadata_url, CA_CERT_PATH, NULL, metadata_response, headers);
	curl_slist_free_all(headers);

	if (curlResult != CURLE_OK) {
		vlog.error("%s: curl error (%d): %s", __func__, (int)curlResult, curl_easy_strerror(curlResult));
		return false;
	}

	vlog.debug("%s: Complete metadata response: %s", __func__, metadata_response.c_str());

	try {
		metadataJson = json::parse(metadata_response);
	} catch (const json::parse_error &e) {
		vlog.error("JSON parsing error: %s", e.what());
		return false;
	}

	return true;
}

static bool isNormalized(const json& metadataJson)
{
	// TODO: What are the other possible results? If it's off, it won't be successful, but what happens if it's on and still not successful?
	if (!metadataJson.contains("result_report")) return false;

	const json& resultReport = metadataJson["result_report"];
	if (!resultReport.contains("files") || !resultReport["files"].is_array()) return false;

	for (const auto& file : resultReport["files"]) {
		if (file.contains("normalized_state") &&
			file["normalized_state"].is_string() &&
			file["normalized_state"].get<std::string>() == "successful")
		{
			return true;
		}
	}

	return false;
}

bool SAutotransferVaithex::processNormalized(
	const VaithexJob& job,
	const char* filePath)
{
	vlog.debug("%s: File was normalized", __func__);
	vlog.debug("%s: removing original file '%s' because it was normalized. Will auto-transfer the normalized file to the client.", __func__, filePath);

	char* nonConstFilePath = strdup(filePath);
	if (!nonConstFilePath) {
		vlog.error("%s: strdup failed for '%s'", __func__, filePath);
		return false;
	}
	delete_file(nonConstFilePath);
	free(nonConstFilePath);

	char result_payload_url[600];
	snprintf(result_payload_url, sizeof(result_payload_url), "https://%s:%s/v1/jobs/%ld/payload", vaithexIp.getData(), vaithexPort.getData(), job.id);

	vlog.debug("Result Payload URL: %s", result_payload_url);

	std::string filename = getBasename(filePath);

	char tarredResult[1024];
	snprintf(tarredResult, sizeof(tarredResult), "/tmp/%s-result.tar", filename.c_str());
	vlog.debug("Value of tarredResult: %s", tarredResult);

	struct curl_slist* headers = NULL;
	std::string tokenHeader = std::string("X-Waas-Token: ") + job.token;
	headers = curl_slist_append(headers, tokenHeader.c_str());
	if (!headers) {
		vlog.error("%s: Failed to create curl header list", __func__);
		return false;
	}

	CURLcode curlResult = curlGetToFile(
		result_payload_url,
		CA_CERT_PATH,
		NULL,
		tarredResult,
		headers);

	curl_slist_free_all(headers);

	if (curlResult != CURLE_OK) {
		vlog.error("Failed to download result payload! curl error (%d): %s", (int)curlResult, curl_easy_strerror(curlResult));
		return false;
	}

	vlog.debug("Result (was cleaned if necessary) archived in %s", tarredResult);
	vlog.debug("Tarred Result Path: %s", tarredResult);

	char* tmpVaithexDir = (char*)malloc(256);
	if (!tmpVaithexDir) {
		vlog.error("%s: Failed to allocate tmpVaithexDir", __func__);
		return false;
	}

	snprintf(tmpVaithexDir, 256, "/tmp/vaithex-tmp-%d", getpid());

	if (mkdir(tmpVaithexDir, 0700) && errno != EEXIST) {
		vlog.error("%s: Failed to create a tgpro temporary directory for autotransfer.", __func__);
		free(tmpVaithexDir);
		return false;
	}

	char* extractedFile = (char*)malloc(4096);
	if (!extractedFile) {
		vlog.error("%s: Failed to allocate extractedFile", __func__);
		cleanupTempDir(tmpVaithexDir);
		free(tmpVaithexDir);
		return false;
	}

	int untarResult = extractAll(tarredResult, tmpVaithexDir, extractedFile);
	if (untarResult != 0) {
		vlog.error("Failed to untar the downloaded payload.");
		free(extractedFile);
		cleanupTempDir(tmpVaithexDir);
		free(tmpVaithexDir);
		return false;
	}

	if (!*extractedFile) {
		vlog.error("%s: failed to get name of normalized file in %s", __func__, tmpVaithexDir);
		free(extractedFile);
		cleanupTempDir(tmpVaithexDir);
		free(tmpVaithexDir);
		return false;
	}

	vlog.debug("%s: extracted normalized file to %s", __func__, extractedFile);

	std::string normalizedFilenameStr = getBasename(extractedFile);
	const char* normalizedFilename = normalizedFilenameStr.c_str();

	vlog.debug("%s: normalized file's filename: %s", __func__, normalizedFilename);

	std::string modifiedFilename = normalizedFilename;
	std::string suffix = getSanitizedVaithexSuffix();
	size_t dotPos = modifiedFilename.find_last_of('.');
	if (dotPos != std::string::npos) {
		modifiedFilename.insert(dotPos, suffix);
	} else {
		modifiedFilename += suffix;
	}

	vlog.debug("Filename %s has been modified to: %s", normalizedFilename, modifiedFilename.c_str());


	std::string autotransferNowFile = "/home/user/.autotransfer-now/" + std::string(getpwuid(geteuid())->pw_name) + "/" + modifiedFilename;

	vlog.debug("%s: trying to mp_move '%s' to '%s'", __func__, extractedFile, autotransferNowFile.c_str());

	vlog.debug("Autotransfer will move file: %s to path: %s", extractedFile, autotransferNowFile.c_str());

	char* nonConstExtractedFile = strdup(extractedFile);
	if (!nonConstExtractedFile) {
		vlog.error("%s: strdup failed for '%s'", __func__, extractedFile);
		free(extractedFile);
		cleanupTempDir(tmpVaithexDir);
		free(tmpVaithexDir);
		return false;
	}
	mp_move(nonConstExtractedFile, const_cast<char*>(autotransferNowFile.c_str()), 1);
	free(nonConstExtractedFile);

	vlog.debug("Autotransfer completed successfully for file: %s at path: %s", modifiedFilename.c_str(), autotransferNowFile.c_str());

	free(extractedFile);
	cleanupTempDir(tmpVaithexDir);
	free(tmpVaithexDir);
	return true;
}

bool SAutotransferVaithex::processClean(const char* filePath)
{
	vlog.debug("Result report: clean");
	std::string autotransferNowFile = "/home/user/.autotransfer-now/" + std::string(getpwuid(geteuid())->pw_name) + "/" + getBasename(filePath);

	vlog.debug("%s: trying to mp_move from '%s' to '%s'", __func__, filePath, autotransferNowFile.c_str());

	char* nonConstFilePath = strdup(filePath);
	if (!nonConstFilePath) {
		vlog.error("%s: strdup failed for '%s'", __func__, filePath);
		return false;
	}

	mp_move(nonConstFilePath, const_cast<char*>(autotransferNowFile.c_str()), 1);
	free(nonConstFilePath);

	vlog.debug("%s: moved '%s' to '%s'", __func__, filePath, autotransferNowFile.c_str());

	return true;
}

bool SAutotransferVaithex::processInfected(const char* filePath, const json& metadataJson)
{
	vlog.debug("Result report: Blocked!");

	delete_file((char*) filePath);

	std::string metadataStr = metadataJson.dump();

	std::string filename = getBasename(filePath);
	vlog.info("%s: VAITHEX rejects %s, scan result: %s", __func__, filename.c_str(), metadataStr.c_str());
	openlog("Xtightgatevnc", LOG_PID, LOG_AUTH);
	syslog(LOG_DEBUG, "%s: VAITHEX rejects %s", __func__, filename.c_str());

	logEvent(filename.c_str(), filePath);

	return true;
}

static int createTarWithSingleFile(const char* tarPath, const char* dir, const char* filename) {
	char inputPath[PATH_MAX];
	snprintf(inputPath, sizeof(inputPath), "%s/%s", dir, filename);

	struct stat st;
	if (stat(inputPath, &st) != 0) {
		vlog.error("%s: stat failed for '%s': %s", __func__, inputPath, strerror(errno));
		return 1;
	}

	if (!S_ISREG(st.st_mode)) {
		vlog.error("%s: refusing to archive non-regular file '%s'", __func__, inputPath);
		return 1;
	}

	struct archive* a = archive_write_new();
	if (!a) {
		vlog.error("%s: archive_write_new failed", __func__);
		return 1;
	}

	archive_write_set_format_pax_restricted(a);

	if (archive_write_open_filename(a, tarPath) != ARCHIVE_OK) {
		vlog.error("%s: failed to open archive '%s': %s", __func__, tarPath, archive_error_string(a));
		archive_write_free(a);
		return 1;
	}

	struct archive_entry* entry = archive_entry_new();
	if (!entry) {
		vlog.error("%s: archive_entry_new failed", __func__);
		archive_write_close(a);
		archive_write_free(a);
		return 1;
	}

	archive_entry_set_pathname(entry, filename);
	archive_entry_set_filetype(entry, AE_IFREG);
	archive_entry_set_perm(entry, 0600);
	archive_entry_set_size(entry, st.st_size);

	if (archive_write_header(a, entry) != ARCHIVE_OK) {
		vlog.error("%s: failed to write archive header: %s", __func__, archive_error_string(a));
		archive_entry_free(entry);
		archive_write_close(a);
		archive_write_free(a);
		return 1;
	}

	int fd = open(inputPath, O_RDONLY);
	if (fd < 0) {
		vlog.error("%s: failed to open input file '%s': %s", __func__, inputPath, strerror(errno));
		archive_entry_free(entry);
		archive_write_close(a);
		archive_write_free(a);
		return 1;
	}

	char buf[16384];
	ssize_t len;
	while ((len = read(fd, buf, sizeof(buf))) > 0) {
		if (archive_write_data(a, buf, len) < 0) {
			vlog.error("%s: failed to write archive data: %s", __func__, archive_error_string(a));
			close(fd);
			archive_entry_free(entry);
			archive_write_close(a);
			archive_write_free(a);
			return 1;
		}
	}

	if (len < 0) {
		vlog.error("%s: failed to read input file '%s': %s", __func__, inputPath, strerror(errno));
		close(fd);
		archive_entry_free(entry);
		archive_write_close(a);
		archive_write_free(a);
		return 1;
	}

	close(fd);
	archive_entry_free(entry);

	if (archive_write_close(a) != ARCHIVE_OK) {
		vlog.error("%s: failed to close archive '%s': %s", __func__, tarPath, archive_error_string(a));
		archive_write_free(a);
		return 1;
	}

	archive_write_free(a);
	return 0;
}

static int extractAll(const char* filename, const char* outputDir, char* lastExtractedFileOutput) {
	vlog.debug("%s: extracting all files from '%s' to '%s'", __func__, filename, outputDir);

	struct archive* a = archive_read_new();
	if (!a) {
		vlog.error("%s: archive_read_new failed", __func__);
		return 1;
	}

	archive_read_support_format_tar(a);
	archive_read_support_filter_all(a);

	if (archive_read_open_filename(a, filename, 10240) != ARCHIVE_OK) {
		vlog.error("%s: failed to open archive '%s': %s", __func__, filename, archive_error_string(a));
		archive_read_free(a);
		return 1;
	}

	struct archive_entry* entry;
	int extractedCount = 0;

	while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
		const char* entryName = archive_entry_pathname(entry);

		// Strip the ./ at the beginning - VAITHEX returns tars with files with a ./ at the beginning
		if (entryName && strncmp(entryName, "./", 2) == 0) {
			entryName += 2;
		}

		if (archive_entry_filetype(entry) == AE_IFDIR) {
			vlog.debug("%s: skipping directory archive entry: %s", __func__, entryName ? entryName : "(null)");
			archive_read_data_skip(a);
			continue;
		}

		if (!entryName || strlen(entryName) == 0) {
			vlog.error("%s: archive contains entry with empty name", __func__);
			archive_read_free(a);
			return 1;
		}

		if (entryName[0] == '/' || strstr(entryName, "..") || strchr(entryName, '/')) {
			vlog.error("%s: refusing unsafe archive entry name: %s", __func__, entryName);
			archive_read_free(a);
			return 1;
		}

		if (archive_entry_filetype(entry) != AE_IFREG) {
			vlog.error("%s: refusing non-regular archive entry: %s", __func__, entryName);
			archive_read_free(a);
			return 1;
		}

		char outputPath[PATH_MAX];
		snprintf(outputPath, sizeof(outputPath), "%s/%s", outputDir, entryName);

		int fd = open(outputPath, O_WRONLY | O_CREAT | O_EXCL, 0600);
		if (fd < 0) {
			vlog.error("%s: failed to create extracted file '%s': %s", __func__, outputPath, strerror(errno));
			archive_read_free(a);
			return 1;
		}

		const void* buff;
		size_t size;
		la_int64_t offset;

		while (true) {
			int r = archive_read_data_block(a, &buff, &size, &offset);
			if (r == ARCHIVE_EOF) {
				break;
			}
			if (r != ARCHIVE_OK) {
				vlog.error("%s: failed to read archive data for '%s': %s", __func__, entryName, archive_error_string(a));
				close(fd);
				archive_read_free(a);
				return 1;
			}

			ssize_t written = write(fd, buff, size);
			if (written < 0 || (size_t)written != size) {
				vlog.error("%s: failed to write extracted file '%s': %s", __func__, outputPath, strerror(errno));
				close(fd);
				archive_read_free(a);
				return 1;
			}
		}

		close(fd);

		snprintf(lastExtractedFileOutput, PATH_MAX, "%s", outputPath);
		extractedCount++;
	}

	archive_read_free(a);

	if (extractedCount != 1) {
		vlog.error("%s: expected exactly one file in archive, extracted %d", __func__, extractedCount);
		return 1;
	}

	return 0;
}

void SAutotransferVaithex::processFile(char *filePath)
{
	VaithexJob job;

	std::string filenameStr = getBasename(filePath);
	std::string dirStr = getDirname(filePath);

	const char* filename = filenameStr.c_str();
	const char* dir = dirStr.c_str();

	vlog.debug("%s: VAITHEXing file '%s' (filename '%s')", __func__, filePath, filename);

	struct stat st;
	if (stat(filePath, &st) != 0) {
		vlog.error("%s: Failed to stat file '%s': %s", __func__, filename, strerror(errno));
		logEvent(filename, "ERROR: FILE_STAT_ERROR");
		return;
	}

	if (st.st_size < 0 || static_cast<uint64_t>(st.st_size) > MAX_VAITHEX_FILE_BYTES) {
		vlog.error(
			"%s: File '%s' is too large for VAITHEX (%lld bytes > %llu bytes), skipping",
			__func__,
			filename,
			static_cast<long long>(st.st_size),
			static_cast<unsigned long long>(MAX_VAITHEX_FILE_BYTES)
		);
		logEvent(filename, "ERROR: FILE_TOO_LARGE");
		return;
	}


	if (mimeChecker && !mimeChecker->checkFile(filePath)) {
		vlog.info("%s: File '%s' rejected by MIME type check, skipping VAITHEX", __func__, filename);
		logEvent(filename, "ERROR: MIME_TYPE_REJECTION_ERROR");
		return;
	}

	// Check return value of createJob
	bool jobCreationSucceeded = createJob(job, static_cast<uint64_t>(st.st_size));
	if (!jobCreationSucceeded) {
		vlog.error("%s: failed to create a job. Aborting VAITHEXing of file '%s'", __func__, filename);
		logEvent(filename, "ERROR: SERVER_JOB_CREATION_ERROR");
		return;
	}
	vlog.debug("Yay! Got me some id (%ld) and some token (%s)", job.id, job.token.c_str());

	// Check return value of uploadPayload
	bool uploadSucceeded = uploadPayload(job, filename, dir);
	if (!uploadSucceeded) {
		vlog.error("%s: Upload failed for file '%s'", __func__, filename);
		logEvent(filename, "ERROR: SERVER_UPLOAD_ERROR");
		cleanupTempTarFiles(filename);
		return;
	}

	json metadataJson;
	bool metadataFetched = fetchMetadata(job, metadataJson);

	if (!metadataFetched || !metadataJson.contains("id")) {
		vlog.error("%s: Failed to fetch valid metadata for file '%s'", __func__, filename);
		logEvent(filename, "ERROR: SERVER_METADATA_DOWNLOAD_ERROR");
		cleanupTempTarFiles(filename);
		return;
	}

	if (!metadataJson.contains("result_report") ||
		!metadataJson["result_report"].contains("result_worst"))
	{
		vlog.error("%s: Missing result_report.result_worst in metadata", __func__);
		logEvent(filename, "ERROR: INVALID_METADATA_RESPONSE");
		cleanupTempTarFiles(filename);
		return;
	}

	if (!metadataJson["result_report"]["result_worst"].is_string()) {
		vlog.error("%s: result_report.result_worst is not a string", __func__);
		logEvent(filename, "ERROR: INVALID_METADATA_RESPONSE");
		cleanupTempTarFiles(filename);
		return;
	}

	std::string resultWorst = metadataJson["result_report"]["result_worst"].get<std::string>();

	bool processed = false;

	if (resultWorst == "infected") {
		processed = processInfected(filePath, metadataJson);
	}
	else if (resultWorst == "clean") {
		if (isNormalized(metadataJson)) {
			processed = processNormalized(job, filePath);
		} else {
			processed = processClean(filePath);
		}
	}
	else {
		vlog.error("%s: Unknown result_worst value: %s", __func__, resultWorst.c_str());
		logEvent(filename, "ERROR: UNKNOWN_SCAN_RESULT");
	}

	cleanupTempTarFiles(filename);

	if (processed) {
		vlog.debug("%s: Processing completed for file '%s'", __func__, filename);
	} else {
		vlog.error("%s: Processing FAILED for file '%s'", __func__, filename);
	}

	return;

}

#endif /* #ifndef WIN32 */
