/* Copyright (C) 2024 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.
 */

#include <cstddef>
#include <cstdlib>
#include <errno.h>
#include <tgpro_environment.h>
#include <rfb/LogWriter.h>

#include "UrlWhitelistHandler.h"

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

const char* WHITELIST_PATH_KEY = "whitelist_path";
const char* FORCE_WHITELIST_PATH_KEY = "force_whitelist_path";

namespace UrlWhitelistHandler {
	char* readAllWhitelistsAsString(const char* viewerConfigDir) {

		const char* filesToConcatenate[4] = { NULL, NULL, NULL, NULL }; // Max. no. of url whitelists we can find

		// If there is a predefined whitelist (defined in vnc file), read only that one
		const char* predefinedWhitelistPath = getWhitelistPathFromConfigFile();
		if (predefinedWhitelistPath && *predefinedWhitelistPath) {
			if (!file_exists(predefinedWhitelistPath) && isForceWhitelistPathFromConfigFile()) {
				wchar_t* predefinedWhitelistPathW = rfb::utf8ToUTF16(predefinedWhitelistPath);
				wchar_t errorMessageW[8192];
				swprintf(errorMessageW, 8192, L"Die eingestellte Whitelist-Datei konnte leider nicht gefunden werden:\n\n%ls", predefinedWhitelistPathW);
				free(predefinedWhitelistPathW);
				error_check(ENOENT, errorMessageW);
			}
			vlog.debug("%s: Found predefined whitelist path (not taking any other whitelist into account): %s", __func__, predefinedWhitelistPath);
			filesToConcatenate[0] = predefinedWhitelistPath;
		} else {
			vlog.debug("%s: No predefined whitelist path (or no match found in it). Looking 'all over the place' (Program Files, %%appdata%%, next to browserchoice.exe and in -configdir if given)", __func__);
			if (viewerConfigDir) { // If we run browserchoice with -configdir, it should be able to look for a whitelist in configdir too
				const char* viewerConfigDirWhitelist = get_url_whitelist_path_viewer_config_dir(viewerConfigDir);
				if (viewerConfigDirWhitelist) {
					vlog.debug("%s: found whitelist in configdir: %s", __func__, viewerConfigDirWhitelist);
					filesToConcatenate[0] = viewerConfigDirWhitelist;
				}
			}

			const char* systemWhitelist = get_url_whitelist_path_system();
			if (systemWhitelist) {
				vlog.debug("%s: found whitelist in C:\\Program Files: %s", __func__, systemWhitelist);
				filesToConcatenate[1] = systemWhitelist;
			}

			const char* userWhitelist = get_url_whitelist_path_user();
			if (userWhitelist) {
				vlog.debug("%s: found user whitelist: %s", __func__, userWhitelist);
				filesToConcatenate[2] = userWhitelist;
			}

			const char* startpathWhitelist = get_url_whitelist_path_startpath();
			if (startpathWhitelist) {
				vlog.debug("%s =? %s", systemWhitelist, startpathWhitelist);

				if (strcmp(systemWhitelist, startpathWhitelist)) {
					vlog.debug("%s: found startpath whitelist (and it's not in C:\\Program Files): %s", __func__, startpathWhitelist);
					filesToConcatenate[3] = startpathWhitelist;
				} else {
					vlog.debug("%s: startpath whitelist is the same as the one in C:\\Program Files. Skipping it...: %s", __func__, startpathWhitelist);
				}
			}

			for (size_t i = 0; i < 4; i++) {
				if (filesToConcatenate[i]) {
					vlog.debug("%s: filesToConcatenate[%ld] -> %s", __func__, i, filesToConcatenate[i]);
				} else {
					vlog.debug("%s: filesToConcatenate[%ld] -> NULL", __func__, i);
				}
			}
		}

		return concatenateFiles(filesToConcatenate, 4);
	}

	char* concatenateFiles(const char** filenames, const size_t numFiles) {
		char* allContent = (char*) malloc(1); // we'll reallocate once we've read one file
		if (!allContent) {
			vlog.error("%s: Failed to allocate memory for concatenation", __func__);
			return NULL;
		}
		allContent[0] = '\0';
		size_t totalSize = 0;



		for (size_t i = 0; i < numFiles; i++) {
			if (!filenames[i]) {
				continue;
			}

			char* fileContent = readWhitelistAsString(filenames[i]);
			if (!fileContent) {
				vlog.error("%s: Failed to read_and_clean file %s", __func__, filenames[i]);
				continue;
			}
			size_t fileLen = strlen(fileContent);
			char* newAllContent = (char*) realloc(allContent, totalSize + fileLen + 1);  // +1 for null terminator
			if (!newAllContent) {
				vlog.error("%s: Failed to reallocate memory during concatenation", __func__);
				free(allContent);
				free(fileContent);
				return NULL;
			}
			allContent = newAllContent;
			strcpy(allContent + totalSize, fileContent);
			totalSize += fileLen;
			free(fileContent);
		}

		return allContent;
	}

	char* readWhitelistAsString(const char* filePath) {
		FILE *file = fopen(filePath, "r");
		if (!file) {
			vlog.error("%s: Failed to open file: %s", __func__, filePath);
			return NULL;
		}

		// Check for UTF-8 BOM (0xEF, 0xBB, 0xBF)
		unsigned char bom[3];
		if (fread(bom, 1, 3, file) == 3) {
			if (!(bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF)) {
				// No BOM detected, seek back to the start
				fseek(file, 0, SEEK_SET);
			}
		} else {
			// Not enough bytes read, seek back to the start
			fseek(file, 0, SEEK_SET);
		}

		size_t bufferSize = 8192;
		char* buffer = (char*) malloc(bufferSize);
		if (!buffer) {
			vlog.error("%s: Failed to allocate memory", __func__);
			fclose(file);
			return NULL;
		}

		size_t cleanedSize = 0;
		char line[4096];
		while (fgets(line, sizeof(line), file)) {
			char* trimmedLine = str_trim(line);

			if (!*trimmedLine || *trimmedLine == '#') {
				continue; // Skip empty lines or comments
			}

			size_t lineLength = strlen(trimmedLine);
			if (cleanedSize + lineLength + 1 > bufferSize) {
				bufferSize *= 2;
				buffer = (char*) realloc(buffer, bufferSize);
				if (!buffer) {
					vlog.error("%s: Failed to reallocate memory", __func__);
					fclose(file);
					return NULL;
				}
			}

			strcpy(buffer + cleanedSize, trimmedLine);
			cleanedSize += lineLength;

			buffer[cleanedSize++] = '\n';
		}

		buffer[cleanedSize] = '\0';

		fclose(file);
		return buffer;
	}

	char* getValueFromBrowserchoiceConfigFile(const char* key) {
		vlog.debug("Looking for browserchoice config file (browserchoice.cfg)");
		const char* browserchoice_config_file = get_browserchoice_cfg_path();
		char* value = NULL;
		if (browserchoice_config_file) {
			value = strdup(get_cfg_value_from_file(key, browserchoice_config_file));
		}
		return value;
	}

	char* getWhitelistPathFromConfigFile() {
		return getValueFromBrowserchoiceConfigFile(WHITELIST_PATH_KEY);
	}

	bool isForceWhitelistPathFromConfigFile() {
		bool forceWhitelistPath = false;
		char* value = getValueFromBrowserchoiceConfigFile(FORCE_WHITELIST_PATH_KEY);
		if (!value || !*value) {
			vlog.debug("%s: Didn't find force_whitelist_path in browserchoice.cfg (assuming 0/false - only affects if whitelist_path is set)", __func__);
		} else {
			vlog.debug("%s: Read force_whitelist_path from browserchoice.cfg: <%s>", __func__, value);
			if (!strcasecmp(value, "1") || !strcasecmp(value, "true")) {
				forceWhitelistPath = true;
			} else {
				forceWhitelistPath = false;
			}
			free(value);
		}
		vlog.debug("%s: Using force_whitelist_path=%u", __func__, forceWhitelistPath);

		return forceWhitelistPath;
	}
}
