/* UnrealIRCd module manager.
 * (C) Copyright 2019 Bram Matthys ("Syzop") and the UnrealIRCd Team.
 * License: GPLv2 or later
 * See https://www.unrealircd.org/docs/Module_manager for user documentation.
 */

#include "unrealircd.h"
#ifndef _WIN32

#define MODULEMANAGER_CONNECT_TIMEOUT	7
#define MODULEMANAGER_TRANSFER_TIMEOUT	20

typedef struct ManagedModule ManagedModule;

struct ManagedModule
{
	ManagedModule *prev, *next;
	char *repo_url;
	char *name;
	char *author;
	char *troubleshooting;
	char *documentation;
	char *version;
	char *source;
	char *sha256sum;
	char *min_unrealircd_version;
	char *max_unrealircd_version;
	char *description;
	MultiLine *post_install_text;
};

static ManagedModule *managed_modules = NULL;

/* We normally do a 'make install' after upgrading a module.
 * However we will skip it if --no-install is done.
 * This is only done during 'make', as it is unexpected to
 * already install modules at the final location before
 * 'make install' was issued.
 */
static int no_make_install = 0;

/* Forward declarations */
int mm_valid_module_name(char *name);
#define safe_free_managed_module(x)	do { free_managed_module(x); x = NULL; } while(0)
void free_managed_module(ManagedModule *m);

typedef enum ParseModuleHeaderStage {
	PMH_STAGE_LOOKING		= 0,
	PMH_STAGE_MODULEHEADER		= 1,
	PMH_STAGE_MOD_HEADER		= 2,
	PMH_STAGE_GOT_NAME		= 3,
	PMH_STAGE_GOT_VERSION		= 4,
	PMH_STAGE_GOT_DESCRIPTION	= 5,
	PMH_STAGE_GOT_AUTHOR		= 6,
	PMT_STAGE_DONE			= 7,
} ParseModuleHeaderStage;

typedef enum ParseModuleConfigStage {
	PMC_STAGE_LOOKING	= 0,
	PMC_STAGE_STARTED	= 1,
	PMC_STAGE_FINISHED	= 2,
} ParseModuleConfigStage;

int parse_quoted_string(char *buf, char *dest, size_t destlen)
{
	char *p, *p2;
	size_t max;
	char *i, *o;

	p = strchr(buf, '"');
	if (!p)
		return 0;
	p2 = strrchr(p+1, '"');
	if (!p2)
		return 0;
	max = p2 - p;
	if (max > destlen)
		max = destlen;
	strlcpy(dest, p+1, max);
	unreal_del_quotes(dest);
	return 1;
}

#undef CheckNull
#define CheckNull(x) if ((!(x)->value) || (!(*((x)->value)))) { config_error("%s:%i: missing parameter", m->name, (x)->line_number); return 0; }

/** Parse a module { } line from a module (not repo!!) */
int mm_module_file_config(ManagedModule *m, ConfigEntry *ce)
{
	ConfigEntry *cep;

	if (ce->value)
	{
		config_error("%s:%d: module { } block should not have a name.",
			m->name, ce->line_number);
		return 0;
	}

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "source") ||
		    !strcmp(cep->name, "version") ||
		    !strcmp(cep->name, "author") ||
		    !strcmp(cep->name, "sha256sum") || 
		    !strcmp(cep->name, "description")
		    )
		{
			config_error("%s:%d: module::%s should not be in here (it only exists in repository entries)",
				m->name, cep->line_number, cep->name);
			return 0;
		}
		else if (!strcmp(cep->name, "troubleshooting"))
		{
			CheckNull(cep);
			safe_strdup(m->troubleshooting, cep->value);
		}
		else if (!strcmp(cep->name, "documentation"))
		{
			CheckNull(cep);
			safe_strdup(m->documentation, cep->value);
		}
		else if (!strcmp(cep->name, "min-unrealircd-version"))
		{
			CheckNull(cep);
			safe_strdup(m->min_unrealircd_version, cep->value);
		}
		else if (!strcmp(cep->name, "max-unrealircd-version"))
		{
			CheckNull(cep);
			safe_strdup(m->max_unrealircd_version, cep->value);
		}
		else if (!strcmp(cep->name, "post-install-text"))
		{
			if (cep->items)
			{
				ConfigEntry *cepp;
				for (cepp = cep->items; cepp; cepp = cepp->next)
					addmultiline(&m->post_install_text, cepp->name);
			} else {
				CheckNull(cep);
				addmultiline(&m->post_install_text, cep->value);
			}
		}
		/* unknown items are silently ignored for future compatibility */
	}

	if (!m->documentation)
	{
		config_error("%s:%d: module::documentation missing", m->name, ce->line_number);
		return 0;
	}

	if (!m->troubleshooting)
	{
		config_error("%s:%d: module::troubleshooting missing", m->name, ce->line_number);
		return 0;
	}

	if (!m->min_unrealircd_version)
	{
		config_error("%s:%d: module::min-unrealircd-version missing", m->name, ce->line_number);
		return 0;
	}

	/* max_unrealircd_version is optional */

	/* post_install_text is optional */

	return 1;
}

#undef CheckNull

int mm_parse_module_file(ManagedModule *m, char *buf, unsigned int line_offset)
{
	ConfigFile *cf;
	ConfigEntry *ce;

	cf = config_parse_with_offset(m->name, buf, line_offset);
	if (!cf)
		return 0; /* eg: parse errors */

	/* Parse the module { } block (only one!) */
	for (ce = cf->items; ce; ce = ce->next)
	{
		if (!strcmp(ce->name, "module"))
		{
			int n = mm_module_file_config(m, ce);
			config_free(cf);
			return n;
		}
	}

	config_free(cf);
	config_error("No module block found within module source file. Contact author.\n");
	return 1;
}

#define MODULECONFIGBUFFER 16384
ManagedModule *mm_parse_module_c_file(char *modulename, char *fname)
{
	char buf[1024];
	FILE *fd;
	ParseModuleHeaderStage parse_module_header = PMH_STAGE_LOOKING;
	ParseModuleConfigStage parse_module_config = PMC_STAGE_LOOKING;
	char *moduleconfig = NULL;
	int linenr = 0, module_config_start_line = 0;
	char module_header_name[128];
	char module_header_version[64];
	char module_header_description[256];
	char module_header_author[128];
	ManagedModule *m = NULL;

	*module_header_name = *module_header_version = *module_header_description = *module_header_author = '\0';

	if (!mm_valid_module_name(modulename))
	{
		fprintf(stderr, "Module file '%s' contains forbidden characters\n", modulename);
		return NULL;
	}

	fd = fopen(fname, "r");
	if (!fd)
	{
		fprintf(stderr, "Unable to open module '%s', file '%s': %s\n",
			modulename, fname, strerror(errno));
		return NULL;
	}

	moduleconfig = safe_alloc(MODULECONFIGBUFFER); /* should be sufficient */
	while ((fgets(buf, sizeof(buf), fd)))
	{
		linenr++;
		stripcrlf(buf);
		/* parse module header stuff: */
		switch (parse_module_header)
		{
			case PMH_STAGE_LOOKING:
				if (strstr(buf, "ModuleHeader"))
					parse_module_header = PMH_STAGE_MODULEHEADER;
				else
					break;
				/*fallthrough*/
			case PMH_STAGE_MODULEHEADER:
				if (strstr(buf, "MOD_HEADER"))
					parse_module_header = PMH_STAGE_MOD_HEADER;
				break;
			case PMH_STAGE_MOD_HEADER:
				if (parse_quoted_string(buf, module_header_name, sizeof(module_header_name)))
					parse_module_header = PMH_STAGE_GOT_NAME;
				break;
			case PMH_STAGE_GOT_NAME:
				if (parse_quoted_string(buf, module_header_version, sizeof(module_header_version)))
					parse_module_header = PMH_STAGE_GOT_VERSION;
				break;
			case PMH_STAGE_GOT_VERSION:
				if (parse_quoted_string(buf, module_header_description, sizeof(module_header_description)))
					parse_module_header = PMH_STAGE_GOT_DESCRIPTION;
				break;
			case PMH_STAGE_GOT_DESCRIPTION:
				if (parse_quoted_string(buf, module_header_author, sizeof(module_header_author)))
					parse_module_header = PMH_STAGE_GOT_AUTHOR;
				break;
			default:
				break;
		}
		/* parse module config stuff: */
		switch (parse_module_config)
		{
			case PMC_STAGE_LOOKING:
				if (strstr(buf, "<<<MODULE MANAGER START>>>")){
					module_config_start_line = linenr;
					parse_module_config = PMC_STAGE_STARTED;
				}
				break;
			case PMC_STAGE_STARTED:
				if (!strstr(buf, "<<<MODULE MANAGER END>>>"))
				{
					strlcat(moduleconfig, buf, MODULECONFIGBUFFER);
					strlcat(moduleconfig, "\n", MODULECONFIGBUFFER);
				} else
				{
					parse_module_config = PMC_STAGE_FINISHED;
				}
				break;
			default:
				/* Nothing to be done anymore */
				break;
		}
	}
	fclose(fd);

	if (!*module_header_name || !*module_header_version ||
	    !*module_header_description || !*module_header_author)
	{
		fprintf(stderr, "Error parsing module header in %s\n", modulename);
		safe_free(moduleconfig);
		return NULL;
	}

	if (strcmp(module_header_name, modulename))
	{
		fprintf(stderr, "ERROR: Mismatch in module name in header (%s) and filename (%s)\n",
			module_header_name, modulename);
		safe_free(moduleconfig);
		return NULL;
	}

	if (!*moduleconfig)
	{
		fprintf(stderr, "ERROR: Module does not contain module config data (<<<MODULE MANAGER START>>>)\n"
		                "This means it is not meant to be managed by the module manager\n");
		safe_free(moduleconfig);
		return NULL;
	}

	/* Fill in the fields from MOD_HEADER() */
	m = safe_alloc(sizeof(ManagedModule));
	safe_strdup(m->name, module_header_name);
	safe_strdup(m->version, module_header_version);
	safe_strdup(m->description, module_header_description);
	safe_strdup(m->author, module_header_author);

	if (!mm_parse_module_file(m, moduleconfig, module_config_start_line))
	{
		fprintf(stderr, "ERROR: Problem with module manager data block within the %s module C source file.\n"
		                "You are suggested to contact the module author and paste the above to him/her\n",
		                m->name);
		safe_free_managed_module(m);
		safe_free(moduleconfig);
		return NULL;
	}
	
	safe_free(moduleconfig);
	return m;
}

void print_documentation(void)
{
	fprintf(stderr, "See https://www.unrealircd.org/docs/Module_manager for more information.\n");
}

char *mm_sourceslist_file(void)
{
	static char buf1[512], buf2[512];
	snprintf(buf1, sizeof(buf1), "%s/modules.sources.list", CONFDIR);
	if (!file_exists(buf1))
	{
		/* Possibly UnrealIRCd is not installed yet, so use this one */
		snprintf(buf2, sizeof(buf2), "%s/doc/conf/modules.sources.list", BUILDDIR);
		if (!file_exists(buf2))
		{
			fprintf(stderr, "ERROR: Neither '%s' nor '%s' exist.\n"
			                "No module repositories configured.\n",
			                buf1, buf2);
			print_documentation();
			exit(-1);
		}
		return buf2;
	}
	return buf1;
}

/** Free a managed module struct */
void free_managed_module(ManagedModule *m)
{
	safe_free(m->repo_url);
	safe_free(m->name);
	safe_free(m->source);
	safe_free(m->sha256sum);
	safe_free(m->version);
	safe_free(m->author);
	safe_free(m->troubleshooting);
	safe_free(m->documentation);
	safe_free(m->min_unrealircd_version);
	safe_free(m->max_unrealircd_version);
	safe_free(m->description);
	freemultiline(m->post_install_text);
	safe_free(m);
}

/** Check for valid module name */
int mm_valid_module_name(char *name)
{
	char *p;

	if (!str_starts_with_case_sensitive(name, "third/"))
		return 0;
	name += 6;
	if (strstr(name, ".."))
		return 0;
	for (p = name; *p; p++)
		if (!isalnum(*p) && !strchr("._-", *p))
			return 0;
	return 1;
}

#undef CheckNull
#define CheckNull(x) if ((!(x)->value) || (!(*((x)->value)))) { config_error("%s:%i: missing parameter", repo_url, (x)->line_number); goto fail_mm_repo_module_config; }

/** Parse a module { } line from a repository */
ManagedModule *mm_repo_module_config(char *repo_url, ConfigEntry *ce)
{
	ConfigEntry *cep;
	ManagedModule *m = safe_alloc(sizeof(ManagedModule));

	if (!ce->value)
	{
		config_error("%s:%d: module { } with no name",
			repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!str_starts_with_case_sensitive(ce->value, "third/"))
	{
		config_error("%s:%d: module { } name must start with: third/",
			repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!mm_valid_module_name(ce->value))
	{
		config_error("%s:%d: module { } with illegal name: %s",
			repo_url, ce->line_number, ce->value);
		goto fail_mm_repo_module_config;
	}
	safe_strdup(m->name, ce->value);
	safe_strdup(m->repo_url, repo_url);

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "source"))
		{
			CheckNull(cep);
			safe_strdup(m->source, cep->value);
		}
		else if (!strcmp(cep->name, "sha256sum"))
		{
			CheckNull(cep);
			safe_strdup(m->sha256sum, cep->value);
		}
		else if (!strcmp(cep->name, "version"))
		{
			CheckNull(cep);
			safe_strdup(m->version, cep->value);
		}
		else if (!strcmp(cep->name, "author"))
		{
			CheckNull(cep);
			safe_strdup(m->author, cep->value);
		}
		else if (!strcmp(cep->name, "troubleshooting"))
		{
			CheckNull(cep);
			safe_strdup(m->troubleshooting, cep->value);
		}
		else if (!strcmp(cep->name, "documentation"))
		{
			CheckNull(cep);
			safe_strdup(m->documentation, cep->value);
		}
		else if (!strcmp(cep->name, "min-unrealircd-version"))
		{
			CheckNull(cep);
			safe_strdup(m->min_unrealircd_version, cep->value);
		}
		else if (!strcmp(cep->name, "max-unrealircd-version"))
		{
			CheckNull(cep);
			safe_strdup(m->max_unrealircd_version, cep->value);
		}
		else if (!strcmp(cep->name, "description"))
		{
			CheckNull(cep);
			safe_strdup(m->description, cep->value);
		}
		else if (!strcmp(cep->name, "post-install-text"))
		{
			if (cep->items)
			{
				ConfigEntry *cepp;
				for (cepp = cep->items; cepp; cepp = cepp->next)
					addmultiline(&m->post_install_text, cepp->name);
			} else {
				CheckNull(cep);
				addmultiline(&m->post_install_text, cep->value);
			}
		}
		/* unknown items are silently ignored for future compatibility */
	}

	if (!m->source)
	{
		config_error("%s:%d: module::source missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->sha256sum)
	{
		config_error("%s:%d: module::sha256sum missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->version)
	{
		config_error("%s:%d: module::version missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->author)
	{
		config_error("%s:%d: module::author missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->documentation)
	{
		config_error("%s:%d: module::documentation missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->troubleshooting)
	{
		config_error("%s:%d: module::troubleshooting missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	if (!m->min_unrealircd_version)
	{
		config_error("%s:%d: module::min-unrealircd-version missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	/* max_unrealircd_version is optional */
	if (!m->description)
	{
		config_error("%s:%d: module::description missing", repo_url, ce->line_number);
		goto fail_mm_repo_module_config;
	}
	/* post_install_text is optional */

	return m;

fail_mm_repo_module_config:
	safe_free_managed_module(m);
	return NULL;
}

#undef CheckNull

int mm_parse_repo_db(char *url, const char *filename)
{
	ConfigFile *cf;
	ConfigEntry *ce;
	ManagedModule *m;

	cf = config_load(filename, url);
	if (!cf)
		return 0; /* eg: parse errors */

	for (ce = cf->items; ce; ce = ce->next)
	{
		if (!strcmp(ce->name, "module"))
		{
			m = mm_repo_module_config(url, ce);
			if (!m)
			{
				config_free(cf);
				return 0;
			}
			AddListItem(m, managed_modules);
		}
	}
	config_free(cf);
	return 1;
}

int mm_refresh_repository(void)
{
	char *sourceslist = mm_sourceslist_file();
	FILE *fd;
	char buf[512];
	const char *tmpfile;
	int linenr = 0;
	int success = 0;
	int numrepos = 0;

	if (!file_exists(TMPDIR))
	{
		(void)mkdir(TMPDIR, S_IRUSR|S_IWUSR|S_IXUSR); /* Create the tmp dir, if it doesn't exist */
		if (!file_exists(TMPDIR))
		{
			/* This is possible if the directory structure does not exist,
			 * eg if ~/unrealircd does not exist at all then ~/unrealircd/tmp
			 * cannot be mkdir'ed either.
			 */
			fprintf(stderr, "ERROR: %s does not exist (yet?), cannot use module manager\n", TMPDIR);
			fprintf(stderr, "       This can only happen if you did not use ./Config or if you rm -rf'ed after running ./Config.\n");
			exit(-1);
		}
	}

	printf("Reading module repository list from '%s'...\n", mm_sourceslist_file());
	fd = fopen(sourceslist, "r");
	if (!fd)
	{
		fprintf(stderr, "ERROR: Could not open '%s': %s\n", sourceslist, strerror(errno));
		return 0;
	}

	while ((fgets(buf, sizeof(buf), fd)))
	{
		char *line = buf;
		linenr++;
		stripcrlf(line);
		/* Skip whitespace */
		while (*line == ' ')
			line++;
		/* Skip empty lines and ones that start with a hash mark (#) */
		if (!*line || (*line == '#'))
			continue;
		if (!str_starts_with_case_sensitive(line, "https://"))
		{
			fprintf(stderr, "ERROR in %s on line %d: URL should start with https://",
				sourceslist, linenr);
			fclose(fd);
			return 0;
		}
		printf("Checking module repository %s...\n", line);
		numrepos++;
		tmpfile = synchronous_http_request(line, 1, MODULEMANAGER_CONNECT_TIMEOUT, MODULEMANAGER_TRANSFER_TIMEOUT);
		if (tmpfile)
		{
			if (!mm_parse_repo_db(line, tmpfile))
			{
				fclose(fd);
				return 0;
			}
			success++;
		}
	}
	fclose(fd);

	if (numrepos == 0)
	{
		fprintf(stderr, "ERROR: No repositories listed in module repository list. "
		                "Did you remove the default UnrealIRCd repository?\n"
		                "All commands, except for './unrealircd module uninstall third/name-of-module', are unavailable.\n");
		return 0;
	}

	return success ? 1 : 0;
}

#define COLUMN_STATUS	0
#define COLUMN_NAME	1
#define COLUMN_VERSION	2

void mm_list_print(char *status, char *name, char *version, char *description, int largest_column[3])
{
	int padstatus = MAX(largest_column[COLUMN_STATUS] - strlen(status), 0);
	int padname = MAX(largest_column[COLUMN_NAME] - strlen(name), 0);
	int padversion = MAX(largest_column[COLUMN_VERSION] - strlen(version), 0);

	printf("| %s%*s | %s%*s | %s%*s | %s\n",
	       status,
	       padstatus, "",
	       name,
	       padname, "",
	       version,
	       padversion, "",
	       description);
}

int mm_check_module_compatibility(ManagedModule *m)
{
	if (strchr(m->min_unrealircd_version, '*'))
	{
		/* By wildcard, eg: "5.*" */
		if (!match_simple(m->min_unrealircd_version, VERSIONONLY))
			return 0;
	} else
	{
		/* By strcmp, eg: "5.0.0" */
		if (strnatcasecmp(m->min_unrealircd_version, VERSIONONLY) > 0)
			return 0;
	}
	if (m->max_unrealircd_version)
	{
		if (strchr(m->max_unrealircd_version, '*'))
		{
			/* By wildcard, eg: "5.*" */
			if (!match_simple(m->max_unrealircd_version, VERSIONONLY))
				return 0;
		} else
		{
			/* By strcmp, eg: "5.0.5" */
			if (strnatcasecmp(m->max_unrealircd_version, VERSIONONLY) <= 0)
				return 0;
		}
	}
	return 1;
}

#define MMMS_INSTALLED		0x0001
#define MMMS_UPGRADE_AVAILABLE	0x0002
#define MMMS_UNAVAILABLE	0x0004

int mm_get_module_status(ManagedModule *m)
{
	FILE *fd;
	char fname[512];
	const char *our_sha256sum;

	snprintf(fname, sizeof(fname), "%s/src/modules/%s.c", BUILDDIR, m->name);
	if (!file_exists(fname))
	{
		if (!mm_check_module_compatibility(m))
			return MMMS_UNAVAILABLE;
		return 0;
	}

	our_sha256sum = sha256sum_file(fname);
	if (!strcasecmp(our_sha256sum, m->sha256sum))
	{
		return MMMS_INSTALLED;
	} else {
		if (!mm_check_module_compatibility(m))
			return MMMS_INSTALLED|MMMS_UNAVAILABLE;
		return MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE;
	}

	return 0;
}

char *mm_get_module_status_string(ManagedModule *m)
{
	int status = mm_get_module_status(m);
	if (status == 0)
		return "";
	else if (status == MMMS_UNAVAILABLE)
		return "unav";
	else if (status == MMMS_INSTALLED)
		return "inst";
	else if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE))
		return "inst/UNAV";
	else if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE))
		return "inst/UPD";
	return "UNKNOWN?";
}

char *mm_get_module_status_string_long(ManagedModule *m)
{
	int status = mm_get_module_status(m);
	if (status == 0)
		return "Not installed";
	else if (status == MMMS_UNAVAILABLE)
		return "Unavailable for your UnrealIRCd version";
	else if (status == MMMS_INSTALLED)
		return "Installed and up to date";
	else if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE))
		return "Installed, an upgrade is available but not for your UnrealIRCd version";
	else if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE))
		return "Installed, upgrade available";
	return "UNKNOWN?";
}

/** Find a module by name, return NULL if not found. */
ManagedModule *mm_find_module(char *name)
{
	ManagedModule *m;

	for (m = managed_modules; m; m = m->next)
		if (!strcasecmp(name, m->name))
			return m;
	return NULL;
}

/** Count the unknown modules (untracked modules) */
int count_unknown_modules(void)
{
	DIR *fd;
	struct dirent *dir;
	int count = 0;
	char dirname[512];

	snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR);

	fd = opendir(dirname);
	if (fd)
	{
		while ((dir = readdir(fd)))
		{
			char *fname = dir->d_name;
			if (filename_has_suffix(fname, ".c"))
			{
				char modname[512], *p;
				snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c"));
				if (!mm_find_module(modname))
					count++;
			}
		}
		closedir(fd);
	}
	return count;
}

void mm_list(char *searchname)
{
	ManagedModule *m;
	int largest_column[3];
	int padname;
	int padversion;
	struct dirent *dir;
	DIR *fd;
	char dirname[512];
	char *status;
	int first_unknown = 1;

	if (searchname)
		printf("Searching for '%s' in names of all available modules...\n", searchname);

	memset(&largest_column, 0, sizeof(largest_column));
	largest_column[COLUMN_STATUS] = strlen("inst/UNAV");
	largest_column[COLUMN_NAME] = strlen("Name:");
	largest_column[COLUMN_VERSION] = strlen("Version:");

	for (m = managed_modules; m; m = m->next)
	{
		if (strlen(m->name) > largest_column[COLUMN_NAME])
			largest_column[COLUMN_NAME] = strlen(m->name);
		if (strlen(m->version) > largest_column[COLUMN_VERSION])
			largest_column[COLUMN_VERSION] = strlen(m->version);
	}
	/* We try to produce neat output, but not at all costs */
	if (largest_column[COLUMN_NAME] > 32)
		largest_column[COLUMN_NAME] = 32;
	if (largest_column[COLUMN_VERSION] > 16)
		largest_column[COLUMN_VERSION] = 16;

	mm_list_print("Status:", "Name:", "Version:", "Description:", largest_column);
	printf("|=======================================================================================\n");

	for (m = managed_modules; m; m = m->next)
	{
		if (searchname && !strstr(m->name, searchname))
			continue;

		status = mm_get_module_status_string(m);
		mm_list_print(status, m->name, m->version, m->description, largest_column);
	}

	snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR);
	fd = opendir(dirname);
	if (fd)
	{
		while ((dir = readdir(fd)))
		{
			char *fname = dir->d_name;
			if (filename_has_suffix(fname, ".c"))
			{
				char modname[512], *p;
				snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c"));
				if (searchname && !strstr(searchname, modname))
					continue;
				if (!mm_find_module(modname))
				{
					if (first_unknown)
					{
						printf("|---------------------------------------------------------------------------------------\n");
						first_unknown = 0;
					}
					mm_list_print("UNKNOWN", modname, "", "", largest_column);
				}
			}
		}
		closedir(fd);
	}

	printf("|=======================================================================================\n");

	printf("\nStatus column legend:\n"
	       "          : not installed\n"
	       "inst      : module installed\n"
	       "inst/UPD  : module installed, upgrade available (latest version differs from yours)\n"
	       "unav      : module not available for your UnrealIRCd version\n"
	       "inst/UNAV : module installed, upgrade exists but is not available for your UnrealIRCd version (too old UnrealIRCd version?)\n"
	       "UNKNOWN   : module does not exist in any repository (perhaps you installed it manually?), module will be left untouched\n");

	printf("\nFor more information about a particular module, use './unrealircd module info name-of-module'\n\n");
	print_documentation();
}

int mm_compile(ManagedModule *m, const char *tmpfile, int test)
{
	char newpath[512];
	char cmd[512];
	const char *basename;
	char *p;
	FILE *fd;
	char buf[512];
	int n;

	if (test)
		printf("Test compiling %s...\n", m->name);
	else
		printf("Compiling %s...\n", m->name);

	basename = unreal_getfilename(test ? tmpfile : m->name);
	snprintf(newpath, sizeof(newpath), "%s/src/modules/third/%s%s", BUILDDIR, basename, test ? "" : ".c");
	if (!test)
	{
		/* If the file already exists then we are upgrading.
		 * It's a good idea to backup the file rather than
		 * just delete it. Perhaps the user had local changes
		 * he/she wished to preserve and accidently went
		 * through the upgrade procedure.
		 */
		char backupfile[512];
		snprintf(backupfile, sizeof(backupfile), "%s.bak", newpath);
		unlink(backupfile);
		(void)rename(newpath, backupfile);
	}
	if (!unreal_copyfileex(tmpfile, newpath, 0))
		return 0;

	snprintf(cmd, sizeof(cmd),
	         "cd \"%s\"; $MAKE custommodule MODULEFILE=\"%s\"",
	         BUILDDIR,
	         filename_strip_suffix(basename, ".c")
	         );
	fd = popen(cmd, "r");
	if (!fd)
	{
		fprintf(stderr, "ERROR: Could not issue command: %s\n", cmd);
		unlink(newpath);
		return 0;
	}
	while((fgets(buf, sizeof(buf), fd)))
	{
		printf("%s", buf);
	}
	n = pclose(fd);

	if (test)
	{
		/* Remove the XXXXXXX.modname.c file */
		unlink(newpath);
		/* Remove the XXXXXXX.modname.so file */
		newpath[strlen(newpath)-2] = '\0'; // cut off .c
		strlcat(newpath, ".so", sizeof(newpath));
		unlink(newpath);
	}

	if (WIFEXITED(n) && (WEXITSTATUS(n) == 0))
		return 1;

	fprintf(stderr, "ERROR: Compile errors encountered while compiling module '%s'\n"
	                "You are suggested to contact the author (%s) of this module:\n%s\n",
	                m->name, m->author, m->troubleshooting);
	return 0;
}

/** Actually download and install the module.
 * This assumes compatibility checks have already been done.
 */
int mm_install_module(ManagedModule *m)
{
	const char *sha256;
	const char *tmpfile;

	printf("Downloading %s from %s...\n", m->name, m->source);
	tmpfile = synchronous_http_request(m->source, 1, MODULEMANAGER_CONNECT_TIMEOUT, MODULEMANAGER_TRANSFER_TIMEOUT);
	if (!tmpfile)
	{
		fprintf(stderr, "Repository %s seems to list a module file that cannot be retrieved (%s).\n", m->repo_url, m->source);
		fprintf(stderr, "Fatal error encountered. Contact %s: %s\n", m->author, m->troubleshooting);
		return 0;
	}

	sha256 = sha256sum_file(tmpfile);
	if (!sha256)
	{
		fprintf(stderr, "ERROR: Temporary file '%s' has disappeared -- strange\n", tmpfile);
		fprintf(stderr, "Fatal error encountered. Check for errors above. Perhaps try running the command again?\n");
		return 0;
	}
	if (strcasecmp(sha256, m->sha256sum))
	{
		fprintf(stderr, "ERROR: SHA256 Checksum mismatch\n"
		                "Expected (value in repository list): %s\n"
		                "Received (value of downloaded file): %s\n",
		                m->sha256sum, sha256);
		fprintf(stderr, "Fatal error encountered, see above. Try running the command again in 5-10 minutes.\n"
		                "If the issue persists, contact the repository manager of %s\n",
		                m->repo_url);
		return 0;
	}
	if (!mm_compile(m, tmpfile, 1))
	{
		fprintf(stderr, "Fatal error encountered, see above.\n");
		return 0;
	}

	if (!mm_compile(m, tmpfile, 0))
	{
		fprintf(stderr, "The test compile went OK earlier, but the final compile did not. BAD!!\n");
		return 0;
	}
	printf("Module %s compiled successfully\n", m->name);
	return 1;
}

/** Uninstall a module.
 * This function takes a string rather than a ManagedModule
 * because it also allows uninstalling of unmanaged (local) modules.
 */
void mm_uninstall_module(char *modulename)
{
	struct dirent *dir;
	DIR *fd;
	char dirname[512], fullname[512];
	int found = 0;

	snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR);
	fd = opendir(dirname);
	if (fd)
	{
		while ((dir = readdir(fd)))
		{
			char *fname = dir->d_name;
			if (filename_has_suffix(fname, ".c") || filename_has_suffix(fname, ".so") || filename_has_suffix(fname, ".dll"))
			{
				char modname[512], *p;
				snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, NULL));
				if (!strcasecmp(modname, modulename))
				{
					found = 1;
					snprintf(fullname, sizeof(fullname), "%s/%s", dirname, fname);
					//printf("Deleting '%s'\n", fullname);
					unlink(fullname);
				}
			}
		}
		closedir(fd);
	}

	if (!found)
	{
		fprintf(stderr, "ERROR: Module '%s' is not installed, so can't uninstall.\n", modulename);
		exit(-1);
	}

	printf("Module '%s' uninstalled successfully\n", modulename);
}

void mm_make_install(void)
{
	char cmd[512];
	int n;
	if (no_make_install)
		return;
	printf("Running 'make install'...\n");
	snprintf(cmd, sizeof(cmd), "cd \"%s\"; $MAKE install 1>/dev/null 2>&1", BUILDDIR);
	n = system(cmd);
}

int mm_install(int argc, char *args[], int upgrade)
{
	ManagedModule *m;
	MultiLine *l;
	char *name = args[1];
	int status;

	if (!name)
	{
		fprintf(stderr, "ERROR: Use: module install third/name-of-module\n");
		return 0;
	}

	if (!str_starts_with_case_sensitive(name, "third/"))
	{
		fprintf(stderr, "ERROR: Use: module install third/name-of-module\nYou must prefix the modulename with third/\n");
		return 0;
	}

	m = mm_find_module(name);
	if (!m)
	{
		fprintf(stderr, "ERROR: Module '%s' not found\n", name);
		return 0;
	}
	status = mm_get_module_status(m);
	if (status == MMMS_UNAVAILABLE)
	{
		fprintf(stderr, "ERROR: Module '%s' exists, but is not compatible with your UnrealIRCd version:\n"
		                "Your UnrealIRCd version  : %s\n"
		                "Minimum version required : %s\n",
		                name,
		                VERSIONONLY,
		                m->min_unrealircd_version);
		if (m->max_unrealircd_version)
			fprintf(stderr, "Maximum version          : %s\n", m->max_unrealircd_version);
		return 0;
	}
	if (upgrade && (status == MMMS_INSTALLED))
	{
		/* If updating, and we are already on latest version, then don't upgrade */
		printf("Module %s is the latest version, no upgrade needed\n", m->name);
	}
	if (!mm_install_module(m))
		return 0;
	mm_make_install();
	if (m->post_install_text)
	{
		printf("Post-installation information for %s from the author:\n", m->name);
		printf("---\n");
		for (l = m->post_install_text; l; l = l->next)
			printf(" %s\n", l->line);
		printf("---\n");
	} else {
		printf("Don't forget to add a 'loadmodule' line for the module and rehash\n");
	}
	return 1;
}

void mm_uninstall(int argc, char *args[])
{
	ManagedModule *m;
	char *name = args[1];

	if (!name)
	{
		fprintf(stderr, "ERROR: Use: module uninstall third/name-of-module\n");
		exit(-1);
	}

	if (!str_starts_with_case_sensitive(name, "third/"))
	{
		fprintf(stderr, "ERROR: Use: module uninstall third/name-of-module\nYou must prefix the modulename with third/\n");
		exit(-1);
	}

	mm_uninstall_module(name);
	mm_make_install();
	exit(0);
}

void mm_upgrade(int argc, char *args[])
{
	ManagedModule *m;
	char *name = args[1];
	int upgraded = 0;
	int failed = 0;
	int uptodate_already = 0;
	int update_unavailable = 0;
	int i = 1, n;

	if (args[i] && !strcmp(args[i], "--no-install"))
	{
		no_make_install = 1;
		i++;
	}

	name = args[i];
	if (name)
	{
		// TODO: First check if it needs an upgrade? ;)
		mm_install(argc, args, 1);
		exit(0);
	}

	/* Without arguments means: check all installed modules */
	no_make_install = 1;
	for (m = managed_modules; m; m = m->next)
	{
		int status = mm_get_module_status(m);
		if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE))
		{
			args[1] = m->name;
			if (mm_install(1, args, 1))
				upgraded++;
			else
				failed++;
		} else
		if (status == MMMS_INSTALLED)
		{
			uptodate_already++;
		} else
		if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE))
		{
			update_unavailable++;
		}
	}
	no_make_install = 0;
	if (upgraded)
		mm_make_install();

	printf("\n\n");
	if (failed)
	{
		printf("There was %d FAILED module upgrade. %d module(s) upgraded, %d already up-to-date\n",
			failed, upgraded, uptodate_already);
	} else {
		printf("All actions were successful. %d module(s) upgraded, %d already up-to-date\n",
			upgraded, uptodate_already);
	}
	if (update_unavailable)
		printf("%d module(s) have updates but not for your UnrealIRCd version\n", update_unavailable);
	if ((n = count_unknown_modules()))
		printf("%d module(s) are unknown/untracked\n", n);

	printf("For more details, you can always run ./unrealircd module list\n");
}

void mm_info(int argc, char *args[])
{
	ManagedModule *m;
	MultiLine *l;
	char *name = args[1];

	if (!name)
	{
		fprintf(stderr, "ERROR: Use: unrealircd module info name-of-module\n");
		exit(-1);
	}

	m = mm_find_module(name);
	if (!m)
	{
		// TODO: we should probably be a bit more specific if the module exists locally (UNAV) */
		fprintf(stderr, "ERROR: Module '%s' not found in any repository\n", name);
		exit(-1);
	}
	printf("Name:                    %s\n"
	       "Version:                 %s\n"
	       "Description:             %s\n"
	       "Author:                  %s\n"
	       "Documentation:           %s\n"
	       "Troubleshooting:         %s\n"
	       "Source:                  %s\n"
	       "Min. UnrealIRCd version: %s\n",
	       m->name,
	       m->version,
	       m->description,
	       m->author,
	       m->documentation,
	       m->troubleshooting,
	       m->source,
	       m->min_unrealircd_version);
	if (m->max_unrealircd_version)
		printf("Min. UnrealIRCd version: %s\n", m->max_unrealircd_version);
	printf("Status:                  %s\n", mm_get_module_status_string_long(m));
	if (m->post_install_text)
	{
		printf("------  Post-installation text  ------\n");
		for (l = m->post_install_text; l; l = l->next)
			printf(" %s\n", l->line);
		printf("------ End of post-install text ------\n");
	}
}

void mm_usage(void)
{
	fprintf(stderr, "Use any of the following actions:\n"
	                "unrealircd module list                     List all the available and installed modules\n"
	                "unrealircd module info name-of-module      Show more information about the module\n"
	                "unrealircd module install name-of-module   Install the specified module\n"
	                "unrealircd module uninstall name-of-module Uninstall the specified module\n"
	                "unrealircd module upgrade name-of-module   Upgrade the specified module (if needed)\n"
	                "unrealircd module upgrade                  Upgrade all modules (if needed)\n"
	                "unrealircd module generate-repository      Generate a repository index (you are\n"
	                "                                           unlikely to need this, only for repo admins)\n");
	print_documentation();
	exit(-1);
}

void print_md_block(FILE *fdo, ManagedModule *m)
{
	fprintf(fdo, "module \"%s\"\n{\n", m->name);
	fprintf(fdo, "\tdescription \"%s\";\n", unreal_add_quotes(m->description));
	fprintf(fdo, "\tversion \"%s\";\n", unreal_add_quotes(m->version));
	fprintf(fdo, "\tauthor \"%s\";\n", unreal_add_quotes(m->author));
	fprintf(fdo, "\tdocumentation \"%s\";\n", unreal_add_quotes(m->documentation));
	fprintf(fdo, "\ttroubleshooting \"%s\";\n", unreal_add_quotes(m->troubleshooting));
	fprintf(fdo, "\tsource \"%s\";\n", unreal_add_quotes(m->source));
	fprintf(fdo, "\tsha256sum \"%s\";\n", unreal_add_quotes(m->sha256sum));
	fprintf(fdo, "\tmin-unrealircd-version \"%s\";\n", unreal_add_quotes(m->min_unrealircd_version));
	if (m->max_unrealircd_version)
		fprintf(fdo, "\tmax-unrealircd-version \"%s\";\n", unreal_add_quotes(m->max_unrealircd_version));
	if (m->post_install_text)
	{
		MultiLine *l;
		fprintf(fdo, "\tpost-install-text\n"
		             "\t{\n");
		for (l = m->post_install_text; l; l = l->next)
			fprintf(fdo, "\t\t\"%s\";\n", unreal_add_quotes(l->line));
		fprintf(fdo, "\t}\n");
	}
	fprintf(fdo, "}\n\n");
}

void mm_generate_repository_usage(void)
{
	fprintf(stderr, "Usage: ./unrealircd module generate-repository <url base path> <directory-with-modules> <name of output file> [optional-minimum-version-filter]\n");
	fprintf(stderr, "For example: ./unrealircd module generate-repository https://www.unrealircd.org/modules/ src/modules/third modules.lst\n");
}

void mm_generate_repository(int argc, char *args[])
{
	DIR *fd;
	struct dirent *dir;
	int count = 0;
	char *urlbasepath;
	char *dirname;
	char *outputfile;
	char *minversion;
	char modname[128];
	char fullname[512];
	ManagedModule *m;
	FILE *fdo;

	urlbasepath = args[1];
	dirname = args[2];
	outputfile = args[3];
	minversion = args[4];

	if (!urlbasepath || !dirname || !outputfile)
	{
		mm_generate_repository_usage();
		exit(-1);
	}

	if ((strlen(urlbasepath) < 2) || (urlbasepath[strlen(urlbasepath)-1] != '/'))
	{
		fprintf(stderr, "Error: the URL base path must end with a slash\n");
		mm_generate_repository_usage();
		exit(-1);
	}

	fd = opendir(dirname);
	if (!fd)
	{
		fprintf(stderr, "Cannot open directory '%s': %s\n", dirname, strerror(errno));
		exit(-1);
	}

	fdo = fopen(outputfile, "w");
	if (!fdo)
	{
		fprintf(stderr, "Could not open file '%s' for writing: %s\n", outputfile, strerror(errno));
		exit(-1);
	}

	while ((dir = readdir(fd)))
	{
		char *fname = dir->d_name;
		if (filename_has_suffix(fname, ".c"))
		{
			int hide = 0;
			snprintf(fullname, sizeof(fullname), "%s/%s", dirname, fname);
			snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c"));
			printf("Processing: %s\n", modname);
			m = mm_parse_module_c_file(modname, fullname);
			if (!m)
			{
				fprintf(stderr, "WARNING: Skipping module '%s' due to errors\n", modname);
				continue;
			}
			m->sha256sum = strdup(sha256sum_file(fullname));
			m->source = safe_alloc(512);
			snprintf(m->source, 512, "%s%s.c", urlbasepath, modname + 6);
			/* filter */
			if (minversion && m->min_unrealircd_version && strncmp(minversion, m->min_unrealircd_version, strlen(minversion)))
				hide = 1;
			/* /filter */
			if (!hide)
				print_md_block(fdo, m);
			safe_free_managed_module(m);
		}
	}
	closedir(fd);
	fclose(fdo);
}

void mm_parse_c_file(int argc, char *args[])
{
	char *fullname = args[1];
	const char *basename;
	char modname[256];
	ManagedModule *m;

	if (!fullname)
	{
		fprintf(stderr, "Usage: ./unrealircd module parse-c-file path/to/file.c\n");
		exit(-1);
	}
	if (!file_exists(fullname))
	{
		fprintf(stderr, "ERROR: Unable to open C file '%s'\n", fullname);
		exit(-1);
	}
	basename = unreal_getfilename(fullname);

	snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(basename, ".c"));
	printf("Processing: %s\n", modname);
	m = mm_parse_module_c_file(modname, fullname);
	if (!m)
	{
		fprintf(stderr, "Errors encountered. See above\n");
		exit(-1);
	}
	m->sha256sum = strdup(sha256sum_file(fullname));
	m->source = strdup("...");
	print_md_block(stdout, m);
	safe_free_managed_module(m);
	exit(0);
}

int mm_detect_make_is_gmake(void)
{
	FILE *fd;
	char buf[512], *s;

	fd = popen("$MAKE --version 2>&1", "r");
	if (fd)
	{
		*buf = '\0';
		s = fgets(buf, sizeof(buf), fd);
		pclose(fd);
		if (s && strstr(s, "GNU Make"))
			return 1; /* Good! We are done. */
	}
	return 0;
}

void mm_detect_make(void)
{
	FILE *fd;
	char *s;
	char buf[512];

	/* Get or set $MAKE */
	s = getenv("MAKE");
	if (!s)
		setenv("MAKE", "make", 1);

	if (mm_detect_make_is_gmake())
		return;

	/* Try again with MAKE=gmake */
	setenv("MAKE", "gmake", 1);
	if (mm_detect_make_is_gmake())
		return;

	fprintf(stderr, "ERROR: GNU Make is not found as 'make' or 'gmake'\n");
	exit(-1);
}

void mm_self_test(void)
{
	char name[512];

	if (!file_exists(BUILDDIR))
	{
		fprintf(stderr, "ERROR: Directory %s does not exist.\n"
				"The UnrealIRCd source is required for the module manager to work!\n",
				BUILDDIR);
		exit(-1);
	} else {
		snprintf(name, sizeof(name), "%s/src/modules/third/Makefile", BUILDDIR);
		if (!file_exists(name))
		{
			fprintf(stderr, "ERROR: Directory %s exists, but your UnrealIRCd is not compiled yet.\n"
					"You must compile your UnrealIRCd first (run './Config', then 'make install')\n",
					BUILDDIR);
			exit(-1);
		}
	}
	mm_detect_make();
}

void modulemanager(int argc, char *args[])
{
	if (!args[0])
		mm_usage();

	mm_self_test();

	/* The following operations do not require reading
	 * of the repository list and are always available:
	 */
	if (!strcasecmp(args[0], "uninstall") ||
	    !strcasecmp(args[0], "remove"))
	{
		mm_uninstall(argc, args);
		exit(0);
	}
	else if (!strcasecmp(args[0], "generate-repository"))
	{
		mm_generate_repository(argc, args);
		exit(0);
	}
	else if (!strcasecmp(args[0], "parse-c-file"))
	{
		mm_parse_c_file(argc, args);
		exit(0);
	}

	/* Fetch the repository list */
	if (!mm_refresh_repository())
	{
		fprintf(stderr, "Fatal error encountered\n");
		exit(-1);
	}

	if (!strcasecmp(args[0], "list"))
		mm_list(args[1]);
	else if (!strcasecmp(args[0], "info"))
		mm_info(argc, args);
	else if (!strcasecmp(args[0], "install"))
	{
		if (mm_install(argc, args, 0))
			fprintf(stderr, "All actions were successful.\n");
	}
	else if (!strcasecmp(args[0], "upgrade"))
		mm_upgrade(argc, args);
	else
		mm_usage();
}
#endif
