#include "./output.h"
#include "./utils_base.h"
#include "../plugins/utils.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// #include <cjson/cJSON.h>
#include "./vendor/cJSON/cJSON.h"
#include "perfdata.h"
#include "states.h"

// == Global variables
static mp_output_format output_format = MP_FORMAT_DEFAULT;
static mp_output_detail_level level_of_detail = MP_DETAIL_ALL;

// == Prototypes ==
static char *fmt_subcheck_output(mp_output_format output_format, mp_subcheck check,
								 unsigned int indentation);
static inline cJSON *json_serialize_subcheck(mp_subcheck subcheck);

// mp_compare_state compares two state arguments
// if *first* is WORSE than *second*, the result is < 0
// if *first* is equal to *second*, the result is 0
// if *first* is BETTER (less bad) the result is > 0
static int mp_compare_state(mp_state_enum first, mp_state_enum second);

// == Implementation ==

// get_subcheck_failed_output retrieves the output of the
// worst and first leave node in a subcheck tree
// or NULL if no such message exists
// the return string is a copy of the original
static char *get_subcheck_failed_output(const mp_subcheck tree) {
	if (tree.subchecks == NULL) {
		// this is a leave node
		if (mp_compute_subcheck_state(tree) == STATE_OK) {
			// ALL OK, nothing to return
			return NULL;
		}

		char *result = strdup(tree.output);
		return result;
	}

	// not a leave node, go through tree
	mp_subcheck_list *subcheck = tree.subchecks;
	mp_subcheck *worst_first_node = NULL;
	mp_state_enum worst_state = STATE_OK;
	while (subcheck != NULL) {
		mp_state_enum current = mp_compute_subcheck_state(subcheck->subcheck);
		if (mp_compare_state(current, worst_state) < 0) {
			worst_first_node = &subcheck->subcheck;
		}

		subcheck = subcheck->next;
	}

	if (worst_first_node == NULL) {
		// we did not find a failed subcheck, return the output
		// of the current node
		char *result = strdup(tree.output);
		return result;
	}

	return get_subcheck_failed_output(*worst_first_node);
}

/*
 * Generate output string for a mp_subcheck object
 */
static inline char *fmt_subcheck_perfdata(mp_subcheck check) {
	char *result = strdup("");
	int added = 0;

	if (check.perfdata != NULL) {
		added = asprintf(&result, "%s", pd_list_to_string(*check.perfdata));
	}

	if (check.subchecks == NULL) {
		// No subchecks, return here
		return result;
	}

	mp_subcheck_list *subchecks = check.subchecks;

	while (subchecks != NULL) {
		if (added > 0) {
			added = asprintf(&result, "%s %s", result, fmt_subcheck_perfdata(subchecks->subcheck));
		} else {
			// TODO free previous result here?
			added = asprintf(&result, "%s", fmt_subcheck_perfdata(subchecks->subcheck));
		}

		subchecks = subchecks->next;
	}

	return result;
}

/*
 * Initialiser for a mp_check object. Always use this to get a new one!
 * It sets useful defaults
 */
mp_check mp_check_init(void) {
	mp_check check = {
		.ok_summary = NULL,
		.evaluation_function = &mp_eval_check_default,
		.default_output_override = NULL,
		.default_output_override_content = NULL,
	};
	return check;
}

/*
 * Initialiser for a mp_subcheck object. Always use this to get a new one!
 * It sets useful defaults
 */
mp_subcheck mp_subcheck_init(void) {
	mp_subcheck tmp = {0};
	tmp.default_state = STATE_UNKNOWN; // Default state is unknown
	tmp.state_set_explicitly = false;
	return tmp;
}

/*
 * Add a subcheck to a (the one and only) check object
 */
int mp_add_subcheck_to_check(mp_check check[static 1], mp_subcheck subcheck) {
	assert(subcheck.output != NULL); // There must be output in a subcheck

	mp_subcheck_list *tmp = NULL;

	if (check->subchecks == NULL) {
		check->subchecks = (mp_subcheck_list *)calloc(1, sizeof(mp_subcheck_list));
		if (check->subchecks == NULL) {
			die(STATE_UNKNOWN, "%s - %s #%d: %s", __FILE__, __func__, __LINE__, "malloc failed");
		}

		check->subchecks->subcheck = subcheck;
		check->subchecks->next = NULL;
	} else {
		// Search for the end
		tmp = (mp_subcheck_list *)calloc(1, sizeof(mp_subcheck_list));
		if (tmp == NULL) {
			die(STATE_UNKNOWN, "%s - %s #%d: %s", __FILE__, __func__, __LINE__, "malloc failed");
		}

		tmp->subcheck = subcheck;
		tmp->next = check->subchecks;

		check->subchecks = tmp;
	}

	return 0;
}

/*
 * Add a mp_perfdata data point to a mp_subcheck object
 */
void mp_add_perfdata_to_subcheck(mp_subcheck check[static 1], const mp_perfdata perfData) {
	if (check->perfdata == NULL) {
		check->perfdata = pd_list_init();
	}
	pd_list_append(check->perfdata, perfData);
}

/*
 * Add a mp_subcheck object to another one. The seconde mp_subcheck (argument) is the lower in the
 * hierarchy
 */
int mp_add_subcheck_to_subcheck(mp_subcheck check[static 1], mp_subcheck subcheck) {
	if (subcheck.output == NULL) {
		die(STATE_UNKNOWN, "%s - %s #%d: %s", __FILE__, __func__, __LINE__,
			"Sub check output is NULL");
	}

	mp_subcheck_list *tmp = NULL;

	if (check->subchecks == NULL) {
		check->subchecks = (mp_subcheck_list *)calloc(1, sizeof(mp_subcheck_list));
		if (check->subchecks == NULL) {
			die(STATE_UNKNOWN, "%s - %s #%d: %s", __FILE__, __func__, __LINE__, "malloc failed");
		}

		tmp = check->subchecks;
	} else {
		// Search for the end
		tmp = check->subchecks;

		while (tmp->next != NULL) {
			tmp = tmp->next;
		}

		tmp->next = (mp_subcheck_list *)calloc(1, sizeof(mp_subcheck_list));
		if (tmp->next == NULL) {
			die(STATE_UNKNOWN, "%s - %s #%d: %s", __FILE__, __func__, __LINE__, "malloc failed");
		}

		tmp = tmp->next;
	}

	tmp->subcheck = subcheck;

	return 0;
}

/*
 * Add a manual summary to a mp_check object, effectively replacing
 * the autogenerated one
 */
void mp_set_summary(mp_check check[static 1], char *summary) { check->summary = strdup(summary); }

/*
 * set the summary for the OK state
 * this allows to set the content in the first line of the plugin
 * if the overall state is OK
 */
void mp_set_ok_summary(mp_check check[static 1], char *ok_summary) {
	check->ok_summary = strdup(ok_summary);
}
/*
 * Generate the summary string of a mp_check object based on its subchecks
 */
char *get_subcheck_summary(mp_check check) {
	mp_subcheck_list *subchecks = check.subchecks;

	unsigned int ok_count = 0;
	unsigned int warning_count = 0;
	unsigned int critical_count = 0;
	unsigned int unknown_count = 0;
	char *result = NULL;
	while (subchecks != NULL) {
		switch (mp_compute_subcheck_state(subchecks->subcheck)) {
		case STATE_OK:
			ok_count++;
			break;
		case STATE_WARNING:
			if (critical_count == 0 && unknown_count == 0 && warning_count == 0) {
				// set summary to first warning subcheck output
				asprintf(&result, "%s", get_subcheck_failed_output(subchecks->subcheck));
			}
			warning_count++;
			break;
		case STATE_CRITICAL:
			if (critical_count == 0) {
				// set summary to first critical subcheck output
				asprintf(&result, "%s", get_subcheck_failed_output(subchecks->subcheck));
			}
			critical_count++;
			break;
		case STATE_UNKNOWN:
			if (critical_count == 0 && unknown_count == 0) {
				// set summary to first unknown subcheck output
				asprintf(&result, "%s", get_subcheck_failed_output(subchecks->subcheck));
			}
			unknown_count++;
			break;
		default:
			die(STATE_UNKNOWN, "Unknown state in get_subcheck_summary");
		}
		subchecks = subchecks->next;
	}

	if (result == NULL) {
		// Nothing in result yet, we must be in an OK state
		if (check.ok_summary != NULL) {
			asprintf(&result, "%s", check.ok_summary);
		} else if (ok_count > 0) {
			asprintf(&result, "ok=%d", ok_count);
		}
	}

	return result;
}

mp_state_enum mp_compute_subcheck_state(const mp_subcheck subcheck) {
	if (subcheck.evaluation_function == NULL) {
		return mp_eval_subcheck_default(subcheck);
	}
	return subcheck.evaluation_function(subcheck);
}

/*
 * Generate the result state of a mp_subcheck object based on its own state and its subchecks
 * states
 */
mp_state_enum mp_eval_subcheck_default(mp_subcheck subcheck) {
	if (subcheck.evaluation_function != NULL) {
		return subcheck.evaluation_function(subcheck);
	}

	if (subcheck.state_set_explicitly) {
		return subcheck.state;
	}

	mp_subcheck_list *scl = subcheck.subchecks;

	if (scl == NULL) {
		return subcheck.default_state;
	}

	mp_state_enum result = STATE_OK;

	while (scl != NULL) {
		result = max_state_alt(result, mp_compute_subcheck_state(scl->subcheck));
		scl = scl->next;
	}

	return result;
}

mp_state_enum mp_compute_check_state(const mp_check check) {
	// just a safety check
	if (check.evaluation_function == NULL) {
		return mp_eval_check_default(check);
	}
	return check.evaluation_function(check);
}

/*
 * Generate the result state of a mp_check object based on its own state and its subchecks states
 */
mp_state_enum mp_eval_check_default(const mp_check check) {
	assert(check.subchecks != NULL); // a mp_check without subchecks is invalid, die here

	mp_subcheck_list *scl = check.subchecks;
	mp_state_enum result = STATE_OK;

	while (scl != NULL) {
		result = max_state_alt(result, mp_compute_subcheck_state(scl->subcheck));
		scl = scl->next;
	}

	return result;
}

// Remove troublesome symbols from plugin output
char *sanitize_output_insitu(char *input) {
	if (input == NULL) {
		return input;
	}

	for (char *walker = input; *walker != '\0'; walker++) {
		if (*walker == '|') {
			*walker = ' ';
		}
	}

	return input;
}

/*
 * Generate output string for a mp_check object
 * Non static to be available for testing functions
 */
char *mp_fmt_output(mp_check check) {
	char *result = NULL;

	switch (output_format) {
	case MP_FORMAT_MULTI_LINE: {
		if (check.default_output_override != NULL) {
			result = check.default_output_override(check.default_output_override_content);
			break;
		}

		if (check.summary == NULL) {
			check.summary = get_subcheck_summary(check);
		}

		asprintf(&result, "[%s] - %s", state_text(mp_compute_check_state(check)), check.summary);

		mp_subcheck_list *subchecks = check.subchecks;

		while (subchecks != NULL) {
			if (level_of_detail == MP_DETAIL_ALL ||
				mp_compute_subcheck_state(subchecks->subcheck) != STATE_OK) {
				asprintf(&result, "%s\n%s", result,
						 fmt_subcheck_output(MP_FORMAT_MULTI_LINE, subchecks->subcheck, 1));
			}
			subchecks = subchecks->next;
		}

		char *pd_string = NULL;
		subchecks = check.subchecks;

		while (subchecks != NULL) {
			if (pd_string == NULL) {
				asprintf(&pd_string, "%s", fmt_subcheck_perfdata(subchecks->subcheck));
			} else {
				asprintf(&pd_string, "%s %s", pd_string,
						 fmt_subcheck_perfdata(subchecks->subcheck));
			}

			subchecks = subchecks->next;
		}

		result = sanitize_output_insitu(result);

		if (pd_string != NULL && strlen(pd_string) > 0) {
			asprintf(&result, "%s|%s", result, pd_string);
		}

		break;
	}
	case MP_FORMAT_TEST_JSON: {
		cJSON *resultObject = cJSON_CreateObject();
		if (resultObject == NULL) {
			die(STATE_UNKNOWN, "cJSON_CreateObject failed");
		}

		cJSON *resultState = cJSON_CreateString(state_text(mp_compute_check_state(check)));
		cJSON_AddItemToObject(resultObject, "state", resultState);

		if (check.summary == NULL) {
			check.summary = get_subcheck_summary(check);
		}

		cJSON *summary = cJSON_CreateString(check.summary);
		cJSON_AddItemToObject(resultObject, "summary", summary);

		if (check.subchecks != NULL) {
			cJSON *subchecks = cJSON_CreateArray();

			mp_subcheck_list *sc = check.subchecks;

			while (sc != NULL) {
				cJSON *sc_json = json_serialize_subcheck(sc->subcheck);
				cJSON_AddItemToArray(subchecks, sc_json);
				sc = sc->next;
			}

			cJSON_AddItemToObject(resultObject, "checks", subchecks);
		}

		result = cJSON_PrintUnformatted(resultObject);
		break;
	}
	default:
		die(STATE_UNKNOWN, "Invalid format");
	}

	return result;
}

/*
 * Helper function to properly indent the output lines when using multiline
 * formats
 */
static char *generate_indentation_string(unsigned int indentation) {
	char *result = calloc(indentation + 1, sizeof(char));

	for (unsigned int i = 0; i < indentation; i++) {
		result[i] = '\t';
	}

	return result;
}

/*
 * Helper function to generate the output string of mp_subcheck
 */
static inline char *fmt_subcheck_output(mp_output_format output_format, mp_subcheck check,
										unsigned int indentation) {
	char *result = NULL;
	mp_subcheck_list *subchecks = NULL;

	switch (output_format) {
	case MP_FORMAT_MULTI_LINE: {
		char *tmp_string = NULL;
		if ((tmp_string = strchr(check.output, '\n')) != NULL) {
			// This is a multiline string, put the correct indentation in before proceeding
			char *intermediate_string = "";
			bool have_residual_chars = false;

			while (tmp_string != NULL) {
				*tmp_string = '\0';
				asprintf(&intermediate_string, "%s%s\n%s", intermediate_string, check.output,
						 generate_indentation_string(
							 indentation + 1)); // one more indentation to make it look better

				if (*(tmp_string + 1) != '\0') {
					check.output = tmp_string + 1;
					have_residual_chars = true;
				} else {
					// Null after the \n, so this is the end
					have_residual_chars = false;
					break;
				}

				tmp_string = strchr(check.output, '\n');
			}

			// add the rest (if any)
			if (have_residual_chars) {
				char *tmp = check.output;
				xasprintf(&check.output, "%s%s%s", intermediate_string,
						  generate_indentation_string(indentation + 1), tmp);
			} else {
				check.output = intermediate_string;
			}
		}
		asprintf(&result, "%s\\_[%s] - %s", generate_indentation_string(indentation),
				 state_text(mp_compute_subcheck_state(check)), check.output);

		subchecks = check.subchecks;

		while (subchecks != NULL) {
			asprintf(&result, "%s\n%s", result,
					 fmt_subcheck_output(output_format, subchecks->subcheck, indentation + 1));
			subchecks = subchecks->next;
		}
		return result;
	}
	default:
		die(STATE_UNKNOWN, "Invalid format");
	}
}

static inline cJSON *json_serialise_pd_value(mp_perfdata_value value) {
	cJSON *result = cJSON_CreateObject();

	switch (value.type) {
	case PD_TYPE_DOUBLE:
		cJSON_AddStringToObject(result, "type", "double");
		break;
	case PD_TYPE_INT:
		cJSON_AddStringToObject(result, "type", "int");
		break;
	case PD_TYPE_UINT:
		cJSON_AddStringToObject(result, "type", "uint");
		break;
	case PD_TYPE_NONE:
		die(STATE_UNKNOWN, "Perfdata type was None in json_serialise_pd_value");
	}
	cJSON_AddStringToObject(result, "value", pd_value_to_string(value));

	return result;
}

static inline cJSON *json_serialise_range(mp_range range) {
	cJSON *result = cJSON_CreateObject();

	if (range.alert_on_inside_range) {
		cJSON_AddBoolToObject(result, "alert_on_inside", true);
	} else {
		cJSON_AddBoolToObject(result, "alert_on_inside", false);
	}

	if (range.end_infinity) {
		cJSON_AddStringToObject(result, "end", "inf");
	} else {
		cJSON_AddItemToObject(result, "end", json_serialise_pd_value(range.end));
	}

	if (range.start_infinity) {
		cJSON_AddStringToObject(result, "start", "inf");
	} else {
		cJSON_AddItemToObject(result, "start", json_serialise_pd_value(range.end));
	}

	return result;
}

static inline cJSON *json_serialise_pd(mp_perfdata pd_val) {
	cJSON *result = cJSON_CreateObject();

	// Label
	cJSON_AddStringToObject(result, "label", pd_val.label);

	// Value
	cJSON_AddItemToObject(result, "value", json_serialise_pd_value(pd_val.value));

	// Uom
	cJSON_AddStringToObject(result, "uom", pd_val.uom);

	// Warn/Crit
	if (pd_val.warn_present) {
		cJSON *warn = json_serialise_range(pd_val.warn);
		cJSON_AddItemToObject(result, "warn", warn);
	}
	if (pd_val.crit_present) {
		cJSON *crit = json_serialise_range(pd_val.crit);
		cJSON_AddItemToObject(result, "crit", crit);
	}

	if (pd_val.min_present) {
		cJSON_AddItemToObject(result, "min", json_serialise_pd_value(pd_val.min));
	}
	if (pd_val.max_present) {
		cJSON_AddItemToObject(result, "max", json_serialise_pd_value(pd_val.max));
	}

	return result;
}

static inline cJSON *json_serialise_pd_list(pd_list *list) {
	cJSON *result = cJSON_CreateArray();

	do {
		cJSON *pd_value = json_serialise_pd(list->data);
		cJSON_AddItemToArray(result, pd_value);
		list = list->next;
	} while (list != NULL);

	return result;
}

static inline cJSON *json_serialize_subcheck(mp_subcheck subcheck) {
	cJSON *result = cJSON_CreateObject();

	// Human readable output
	cJSON *output = cJSON_CreateString(subcheck.output);
	cJSON_AddItemToObject(result, "output", output);

	// Test state (aka Exit Code)
	cJSON *state = cJSON_CreateString(state_text(mp_compute_subcheck_state(subcheck)));
	cJSON_AddItemToObject(result, "state", state);

	// Perfdata
	if (subcheck.perfdata != NULL) {
		cJSON *perfdata = json_serialise_pd_list(subcheck.perfdata);
		cJSON_AddItemToObject(result, "perfdata", perfdata);
	}

	if (subcheck.subchecks != NULL) {
		cJSON *subchecks = cJSON_CreateArray();

		mp_subcheck_list *sc = subcheck.subchecks;

		while (sc != NULL) {
			cJSON *sc_json = json_serialize_subcheck(sc->subcheck);
			cJSON_AddItemToArray(subchecks, sc_json);
			sc = sc->next;
		}

		cJSON_AddItemToObject(result, "checks", subchecks);
	}

	return result;
}

/*
 * Wrapper function to print the output string of a mp_check object
 * Use this in concrete plugins.
 */
void mp_print_output(mp_check check) { puts(mp_fmt_output(check)); }

/*
 * Convenience function to print the output string of a mp_check object and exit
 * the program with the resulting state.
 * Intended to be used to exit a monitoring plugin.
 */
void mp_exit(mp_check check) {
	mp_print_output(check);
	if (output_format == MP_FORMAT_TEST_JSON) {
		exit(0);
	}

	exit(mp_compute_check_state(check));
}

/*
 * Function to set the result state of a mp_subcheck object explicitly.
 * This will overwrite the default state AND states derived from it's subchecks
 */
mp_subcheck mp_set_subcheck_state(mp_subcheck check, mp_state_enum state) {
	check.state = state;
	check.state_set_explicitly = true;
	return check;
}

/*
 * Function to set the default result state of a mp_subcheck object. This state
 * will be used if neither an explicit state is set (see *mp_set_subcheck_state*)
 * nor does it include other subchecks
 */
mp_subcheck mp_set_subcheck_default_state(mp_subcheck check, mp_state_enum state) {
	check.default_state = state;
	return check;
}

char *mp_output_format_map[] = {
	[MP_FORMAT_MULTI_LINE] = "multi-line",
	[MP_FORMAT_TEST_JSON] = "mp-test-json",
};

/*
 * Function to parse the output from a string
 */
parsed_output_format mp_parse_output_format(char *format_string) {
	parsed_output_format result = {
		.parsing_success = false,
		.output_format = MP_FORMAT_DEFAULT,
	};

	for (mp_output_format i = 0; i < (sizeof(mp_output_format_map) / sizeof(char *)); i++) {
		if (strcasecmp(mp_output_format_map[i], format_string) == 0) {
			result.parsing_success = true;
			result.output_format = i;
			break;
		}
	}

	return result;
}

void mp_set_format(mp_output_format format) { output_format = format; }

mp_output_format mp_get_format(void) { return output_format; }

void mp_set_level_of_detail(mp_output_detail_level level) { level_of_detail = level; }

mp_output_detail_level mp_get_level_of_detail(void) { return level_of_detail; }

mp_state_enum mp_eval_ok(mp_check overall) {
	(void)overall;
	return STATE_OK;
}

mp_state_enum mp_eval_warning(mp_check overall) {
	(void)overall;
	return STATE_WARNING;
}

mp_state_enum mp_eval_critical(mp_check overall) {
	(void)overall;
	return STATE_CRITICAL;
}

mp_state_enum mp_eval_unknown(mp_check overall) {
	(void)overall;
	return STATE_UNKNOWN;
}

static int mp_compare_state(mp_state_enum first, mp_state_enum second) {
	switch (first) {
	case STATE_OK:
		switch (second) {
		case STATE_OK:
			return 0;
		case STATE_WARNING:
		case STATE_UNKNOWN:
		case STATE_CRITICAL:
			return 1;
		}
	case STATE_WARNING:
		switch (second) {
		case STATE_OK:
			return -1;
		case STATE_WARNING:
			return 0;
		case STATE_UNKNOWN:
		case STATE_CRITICAL:
			return 1;
		}
	case STATE_UNKNOWN:
		switch (second) {
		case STATE_OK:
		case STATE_WARNING:
			return -1;
		case STATE_UNKNOWN:
			return 0;
		case STATE_CRITICAL:
			return 1;
		}
	case STATE_CRITICAL:
		switch (second) {
		case STATE_OK:
		case STATE_WARNING:
		case STATE_UNKNOWN:
			return -1;
		case STATE_CRITICAL:
			return 0;
		}
	}
}
