// SPDX-FileCopyrightText: 2012 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "livestatus/livestatuslogutility.hpp"
#include "icinga/service.hpp"
#include "icinga/host.hpp"
#include "icinga/user.hpp"
#include "icinga/checkcommand.hpp"
#include "icinga/eventcommand.hpp"
#include "icinga/notificationcommand.hpp"
#include "base/utility.hpp"
#include "base/convert.hpp"
#include "base/logger.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <fstream>

using namespace icinga;

void LivestatusLogUtility::CreateLogIndex(const String& path, std::map<time_t, String>& index)
{
	Utility::Glob(path + "/icinga.log", [&index](const String& newPath) { CreateLogIndexFileHandler(newPath, index); }, GlobFile);
	Utility::Glob(path + "/archives/*.log", [&index](const String& newPath) { CreateLogIndexFileHandler(newPath, index); }, GlobFile);
}

void LivestatusLogUtility::CreateLogIndexFileHandler(const String& path, std::map<time_t, String>& index)
{
	std::ifstream stream;
	stream.open(path.CStr(), std::ifstream::in);

	if (!stream)
		BOOST_THROW_EXCEPTION(std::runtime_error("Could not open log file: " + path));

	/* read the first bytes to get the timestamp: [123456789] */
	char buffer[12];

	stream.read(buffer, 12);

	if (buffer[0] != '[' || buffer[11] != ']') {
		/* this can happen for directories too, silently ignore them */
		return;
	}

	/* extract timestamp */
	buffer[11] = 0;
	time_t ts_start = atoi(buffer+1);

	stream.close();

	Log(LogDebug, "LivestatusLogUtility")
		<< "Indexing log file: '" << path << "' with timestamp start: '" << ts_start << "'.";

	index[ts_start] = path;
}

void LivestatusLogUtility::CreateLogCache(std::map<time_t, String> index, HistoryTable *table,
	time_t from, time_t until, const AddRowFunction& addRowFn)
{
	ASSERT(table);

	/* m_LogFileIndex map tells which log files are involved ordered by their start timestamp */
	for (const auto& kv : index) {
		unsigned int ts = kv.first;

		/* skip log files not in range (performance optimization) */
		if (ts < from || ts > until)
			continue;

		String log_file = index[ts];
		int lineno = 0;

		std::ifstream fp;
		fp.exceptions(std::ifstream::badbit);
		fp.open(log_file.CStr(), std::ifstream::in);

		while (fp.good()) {
			std::string line;
			std::getline(fp, line);

			if (line.empty())
				continue; /* Ignore empty lines */

			Dictionary::Ptr log_entry_attrs = LivestatusLogUtility::GetAttributes(line);

			/* no attributes available - invalid log line */
			if (!log_entry_attrs) {
				Log(LogDebug, "LivestatusLogUtility")
					<< "Skipping invalid log line: '" << line << "'.";
				continue;
			}

			table->UpdateLogEntries(log_entry_attrs, lineno, addRowFn);

			lineno++;
		}

		fp.close();
	}
}

Dictionary::Ptr LivestatusLogUtility::GetAttributes(const String& text)
{
	Dictionary::Ptr bag = new Dictionary();

	/*
	 * [1379025342] SERVICE NOTIFICATION: contactname;hostname;servicedesc;WARNING;true;foo output
	 */
	unsigned long time = atoi(text.SubStr(1, 11).CStr());

	Log(LogDebug, "LivestatusLogUtility")
		<< "Processing log line: '" << text << "'.";
	bag->Set("time", time);

	size_t colon = text.FindFirstOf(':');
	size_t colon_offset = colon - 13;

	String type = String(text.SubStr(13, colon_offset)).Trim();
	String options = String(text.SubStr(colon + 1)).Trim();

	bag->Set("type", type);
	bag->Set("options", options);

	std::vector<String> tokens = options.Split(";");

	/* set default values */
	bag->Set("class", LogEntryClassInfo);
	bag->Set("log_type", 0);
	bag->Set("state", 0);
	bag->Set("attempt", 0);
	bag->Set("message", text); /* used as 'message' in log table, and 'log_output' in statehist table */

	if (type.Contains("INITIAL HOST STATE") ||
		type.Contains("CURRENT HOST STATE") ||
		type.Contains("HOST ALERT")) {
		if (tokens.size() < 5)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("state", Host::StateFromString(tokens[1]));
		bag->Set("state_type", tokens[2]);
		bag->Set("attempt", atoi(tokens[3].CStr()));
		bag->Set("plugin_output", tokens[4]);

		if (type.Contains("INITIAL HOST STATE")) {
			bag->Set("class", LogEntryClassState);
			bag->Set("log_type", LogEntryTypeHostInitialState);
		}
		else if (type.Contains("CURRENT HOST STATE")) {
			bag->Set("class", LogEntryClassState);
			bag->Set("log_type", LogEntryTypeHostCurrentState);
		}
		else {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeHostAlert);
		}

		return bag;
	} else if (type.Contains("HOST DOWNTIME ALERT") ||  type.Contains("HOST FLAPPING ALERT")) {
		if (tokens.size() < 3)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("state_type", tokens[1]);
		bag->Set("comment", tokens[2]);

		if (type.Contains("HOST FLAPPING ALERT")) {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeHostFlapping);
		} else {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeHostDowntimeAlert);
		}

		return bag;
	} else if (type.Contains("INITIAL SERVICE STATE") ||
		type.Contains("CURRENT SERVICE STATE") ||
		type.Contains("SERVICE ALERT")) {
		if (tokens.size() < 6)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("service_description", tokens[1]);
		bag->Set("state", Service::StateFromString(tokens[2]));
		bag->Set("state_type", tokens[3]);
		bag->Set("attempt", atoi(tokens[4].CStr()));
		bag->Set("plugin_output", tokens[5]);

		if (type.Contains("INITIAL SERVICE STATE")) {
			bag->Set("class", LogEntryClassState);
			bag->Set("log_type", LogEntryTypeServiceInitialState);
		}
		else if (type.Contains("CURRENT SERVICE STATE")) {
			bag->Set("class", LogEntryClassState);
			bag->Set("log_type", LogEntryTypeServiceCurrentState);
		}
		else {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeServiceAlert);
		}

		return bag;
	} else if (type.Contains("SERVICE DOWNTIME ALERT") ||
		type.Contains("SERVICE FLAPPING ALERT")) {
		if (tokens.size() < 4)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("service_description", tokens[1]);
		bag->Set("state_type", tokens[2]);
		bag->Set("comment", tokens[3]);

		if (type.Contains("SERVICE FLAPPING ALERT")) {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeServiceFlapping);
		} else {
			bag->Set("class", LogEntryClassAlert);
			bag->Set("log_type", LogEntryTypeServiceDowntimeAlert);
		}

		return bag;
	} else if (type.Contains("TIMEPERIOD TRANSITION")) {
		if (tokens.size() < 4)
			return bag;

		bag->Set("class", LogEntryClassState);
		bag->Set("log_type", LogEntryTypeTimeperiodTransition);

		bag->Set("host_name", tokens[0]);
		bag->Set("service_description", tokens[1]);
		bag->Set("state_type", tokens[2]);
		bag->Set("comment", tokens[3]);
	} else if (type.Contains("HOST NOTIFICATION")) {
		if (tokens.size() < 6)
			return bag;

		bag->Set("contact_name", tokens[0]);
		bag->Set("host_name", tokens[1]);
		bag->Set("state_type", tokens[2].CStr());
		bag->Set("state", Service::StateFromString(tokens[3]));
		bag->Set("command_name", tokens[4]);
		bag->Set("plugin_output", tokens[5]);

		bag->Set("class", LogEntryClassNotification);
		bag->Set("log_type", LogEntryTypeHostNotification);

		return bag;
	} else if (type.Contains("SERVICE NOTIFICATION")) {
		if (tokens.size() < 7)
			return bag;

		bag->Set("contact_name", tokens[0]);
		bag->Set("host_name", tokens[1]);
		bag->Set("service_description", tokens[2]);
		bag->Set("state_type", tokens[3].CStr());
		bag->Set("state", Service::StateFromString(tokens[4]));
		bag->Set("command_name", tokens[5]);
		bag->Set("plugin_output", tokens[6]);

		bag->Set("class", LogEntryClassNotification);
		bag->Set("log_type", LogEntryTypeServiceNotification);

		return bag;
	} else if (type.Contains("PASSIVE HOST CHECK")) {
		if (tokens.size() < 3)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("state", Host::StateFromString(tokens[1]));
		bag->Set("plugin_output", tokens[2]);

		bag->Set("class", LogEntryClassPassive);

		return bag;
	} else if (type.Contains("PASSIVE SERVICE CHECK")) {
		if (tokens.size() < 4)
			return bag;

		bag->Set("host_name", tokens[0]);
		bag->Set("service_description", tokens[1]);
		bag->Set("state", Host::StateFromString(tokens[2]));
		bag->Set("plugin_output", tokens[3]);

		bag->Set("class", LogEntryClassPassive);

		return bag;
	} else if (type.Contains("EXTERNAL COMMAND")) {
		bag->Set("class", LogEntryClassCommand);
		/* string processing not implemented in 1.x */

		return bag;
	} else if (type.Contains("LOG VERSION")) {
		bag->Set("class", LogEntryClassProgram);
		bag->Set("log_type", LogEntryTypeVersion);

		return bag;
	} else if (type.Contains("logging initial states")) {
		bag->Set("class", LogEntryClassProgram);
		bag->Set("log_type", LogEntryTypeInitialStates);

		return bag;
	} else if (type.Contains("starting... (PID=")) {
		bag->Set("class", LogEntryClassProgram);
		bag->Set("log_type", LogEntryTypeProgramStarting);

		return bag;
	}
	/* program */
	else if (type.Contains("restarting...") ||
		type.Contains("shutting down...") ||
		type.Contains("Bailing out") ||
		type.Contains("active mode...") ||
		type.Contains("standby mode...")) {
		bag->Set("class", LogEntryClassProgram);

		return bag;
	}

	return bag;
}
