/*
 * Copyright (c) 2005-2006 Atheme Development Group
 * Rights to this code are as documented in doc/LICENSE.
 *
 * This file contains the implementation of the Atheme 0.1
 * flatfile database format, with metadata extensions.
 *
 */

#include "atheme.h"
#include "template.h"

DECLARE_MODULE_V1
(
	"backend/flatfile", true, _modinit, NULL,
	PACKAGE_STRING,
	"Atheme Development Group <http://www.atheme.org>"
);

/* database versions */
#define DB_SHRIKE	1
#define DB_ATHEME	2

/* loads atheme.db */
static void flatfile_db_load(const char *filename)
{
	myuser_t *mu, *founder = NULL;
	myuser_name_t *mun;
	mychan_t *mc;
	kline_t *k;
	xline_t *x;
	qline_t *q;
	svsignore_t *svsignore;
	unsigned int versn = 0, i;
	unsigned int linecnt = 0, muin = 0, mcin = 0, cain = 0, kin = 0, xin = 0, qin = 0;
	FILE *f;
	char *item, *s, *buf;
	size_t bufsize = BUFSIZE, n;
	int c;
	unsigned int their_ca_all = 0;
	char path[BUFSIZE];

	snprintf(path, BUFSIZE, "%s/%s", datadir, filename != NULL ? filename : "atheme.db");
	f = fopen(path, "r");
	if (f == NULL)
	{
		if (errno == ENOENT)
		{
			slog(LG_ERROR, "db_load(): %s does not exist.  as no data will be converted, you should use the 'opensex' backend now.", path);
			exit(EXIT_FAILURE);
		}
		else
		{
			slog(LG_ERROR, "db_load(): can't open %s for reading: %s", path, strerror(errno));
			slog(LG_ERROR, "db_load(): exiting to avoid data loss");
			exit(EXIT_FAILURE);
		}
	}

	slog(LG_DEBUG, "db_load(): ----------------------- loading ------------------------");

	buf = smalloc(bufsize);

	/* start reading it, one line at a time */
	for (;;)
	{
		n = 0;
		while ((c = getc(f)) != EOF && c != '\n')
		{
			buf[n++] = c;
			if (n == bufsize)
			{
				bufsize *= 2;
				buf = srealloc(buf, bufsize);
			}
		}
		buf[n] = '\0';

		if (c == EOF && ferror(f))
		{
			slog(LG_ERROR, "db_load(): error while reading %s: %s", path, strerror(errno));
			slog(LG_ERROR, "db_load(): exiting to avoid data loss");
			exit(EXIT_FAILURE);
		}

		if (c == EOF && n == 0)
			break;

		linecnt++;

		/* check for unimportant lines */
		item = strtok(buf, " ");
		strip(item);

		if (item == NULL || *item == '#' || *item == '\n' || *item == '\t' || *item == ' ' || *item == '\0' || *item == '\r')
			continue;

		/* database version */
		if (!strcmp("DBV", item))
		{
			versn = atoi(strtok(NULL, " "));
			if (versn > 6)
			{
				slog(LG_INFO, "db_load(): database version is %d; i only understand 5 (Atheme 2.0, 2.1), "
					"4 (Atheme 0.2), 3 (Atheme 0.2 without CA_ACLVIEW), 2 (Atheme 0.1) or 1 (Shrike)", versn);
				exit(EXIT_FAILURE);
			}
		}
		else if (!strcmp("CF", item))
		{
			/* enabled chanacs flags */
			s = strtok(NULL, " ");
			if (s == NULL)
				slog(LG_INFO, "db_load(): missing param to CF");
			else
			{
				their_ca_all = flags_to_bitmask(s, 0);
				if (their_ca_all & ~ca_all)
				{
					slog(LG_ERROR, "db_load(): losing flags %s from file", bitmask_to_flags(their_ca_all & ~ca_all));
				}
				if (~their_ca_all & ca_all)
				{
					slog(LG_ERROR, "db_load(): making up flags %s not present in file", bitmask_to_flags(~their_ca_all & ca_all));
				}
			}
		}
		else if (!strcmp("MU", item))
		{
			/* myusers */
			char *muname, *mupass, *muemail;

			if ((s = strtok(NULL, " ")))
			{
				/* We need to know the flags before we myuser_add,
				 * so we need a few temporary places to put stuff.
				 */
				unsigned int registered, lastlogin;
				char *failnum, *lastfailaddr, *lastfailtime;
				char *flagstr, *language;

				if (myuser_find(s))
				{
					slog(LG_INFO, "db_load(): skipping duplicate account %s (line %d)", s, linecnt);
					continue;
				}

				muin++;

				muname = s;
				mupass = strtok(NULL, " ");
				muemail = strtok(NULL, " ");

				registered = atoi(strtok(NULL, " "));
				lastlogin = atoi(strtok(NULL, " "));
				failnum = strtok(NULL, " ");
				lastfailaddr = strtok(NULL, " ");
				lastfailtime = strtok(NULL, " ");
				flagstr = strtok(NULL, " ");
				language = strtok(NULL, " ");

				mu = myuser_add(muname, mupass, muemail, atol(flagstr));

				mu->registered = registered;
				mu->lastlogin = lastlogin;

				if (strcmp(failnum, "0"))
					metadata_add(mu, "private:loginfail:failnum", failnum);
				if (strcmp(lastfailaddr, "0"))
					metadata_add(mu, "private:loginfail:lastfailaddr", lastfailaddr);
				if (strcmp(lastfailtime, "0"))
					metadata_add(mu, "private:loginfail:lastfailtime", lastfailtime);

				/* Verification keys were moved to metadata,
				 * but we'll still accept them from legacy
				 * databases. Note that we only transfer over
				 * initial registration keys -- I don't think
				 * we saved mu->temp, so we can't transition
				 * e-mail change keys anyway.     --alambert
				 */
				if ((s = strtok(NULL, " ")))
				{
					strip(s);
					metadata_add(mu, "private:verify:register:key", s);
					metadata_add(mu, "private:verify:register:timestamp", "0");
				}

				/* Create entries for languages in the db
				 * even if we do not have catalogs for them.
				 */
				if (language != NULL)
					mu->language = language_add(language);
			}
		}
		else if (!strcmp("ME", item))
		{
			/* memo */
			char *sender, *text;
			time_t mtime;
			unsigned int status;
			mymemo_t *mz;

			mu = myuser_find(strtok(NULL, " "));
			sender = strtok(NULL, " ");
			mtime = atoi(strtok(NULL, " "));
			status = atoi(strtok(NULL, " "));
			text = strtok(NULL, "\n");

			if (!mu)
			{
				slog(LG_DEBUG, "db_load(): memo for unknown account");
				continue;
			}

			if (!sender || !mtime || !text)
				continue;

			mz = smalloc(sizeof(mymemo_t));

			mowgli_strlcpy(mz->sender, sender, NICKLEN);
			mowgli_strlcpy(mz->text, text, MEMOLEN);
			mz->sent = mtime;
			mz->status = status;

			if (!(mz->status & MEMO_READ))
				mu->memoct_new++;

			mowgli_node_add(mz, mowgli_node_create(), &mu->memos);
		}
		else if (!strcmp("MI", item))
		{
			/* memo ignore */
			char *user, *target, *strbuf;

			user = strtok(NULL, " ");
			target = strtok(NULL, "\n");

			mu = myuser_find(user);
			
			if (!mu)
			{
				slog(LG_DEBUG, "db_load(): invalid ignore (MI %s %s)", user,target);
				continue;
			}
			
			strbuf = sstrdup(target);
			
			mowgli_node_add(strbuf, mowgli_node_create(), &mu->memo_ignores);
		}
		else if (!strcmp("AC", item))
		{
			/* myuser access list */
			char *user, *mask;

			user = strtok(NULL, " ");
			mask = strtok(NULL, "\n");

			mu = myuser_find(user);

			if (mu == NULL)
			{
				slog(LG_DEBUG, "db_load(): invalid access entry<%s> for unknown user<%s>", mask, user);
				continue;
			}

			myuser_access_add(mu, mask);
		}
		else if (!strcmp("MN", item))
		{
			/* registered nick */
			char *user, *nick, *treg, *tseen;
			mynick_t *mn;

			user = strtok(NULL, " ");
			nick = strtok(NULL, " ");
			treg = strtok(NULL, " ");
			tseen = strtok(NULL, " ");

			mu = myuser_find(user);
			if (mu == NULL || nick == NULL || tseen == NULL)
			{
				slog(LG_DEBUG, "db_load(): invalid registered nick<%s> for user<%s>", nick, user);
				continue;
			}

			if (mynick_find(nick))
			{
				slog(LG_INFO, "db_load(): skipping duplicate nick %s (account %s) (line %d)", nick, user, linecnt);
				continue;
			}

			mn = mynick_add(mu, nick);
			mn->registered = atoi(treg);
			mn->lastseen = atoi(tseen);
		}
		else if (!strcmp("MCFP", item))
		{
			/* certfp */
			char *user, *certfp;
			mycertfp_t *mcfp;

			user = strtok(NULL, " ");
			certfp = strtok(NULL, " ");

			mu = myuser_find(user);
			if (mu == NULL || certfp == NULL)
			{
				slog(LG_DEBUG, "db_load(): invalid certfp<%s> for user<%s>", certfp, user);
				continue;
			}

			mcfp = mycertfp_add(mu, certfp);
		}
		else if (!strcmp("NAM", item))
		{
			/* formerly registered name (created by a marked account being dropped) */
			char *user;

			user = strtok(NULL, " \n");
			if (!user)
			{
				slog(LG_INFO, "db_load(): invalid old name (line %d)", linecnt);
				continue;
			}
			myuser_name_add(user);
		}
		else if (!strcmp("SO", item))
		{
			/* services oper */
			char *user, *class, *flagstr, *password;

			user = strtok(NULL, " ");
			class = strtok(NULL, " ");
			flagstr = strtok(NULL, " \n");
			password = strtok(NULL, "\n");

			mu = myuser_find(user);

			if (!mu || !class || !flagstr)
			{
				slog(LG_DEBUG, "db_load(): invalid services oper (SO %s %s %s)", user, class, flagstr);
				continue;
			}
			soper_add(entity(mu)->name, class, atoi(flagstr) & ~SOPER_CONF, password);
		}
		else if (!strcmp("MC", item))
		{
			/* mychans */
			char *mcname;

			if ((s = strtok(NULL, " ")))
			{
				if (mychan_find(s))
				{
					slog(LG_INFO, "db_load(): skipping duplicate channel %s (line %d)", s, linecnt);
					continue;
				}

				mcin++;

				mcname = s;
				/* unused (old password) */
				(void)strtok(NULL, " ");

				mc = mychan_add(mcname);

				founder = myuser_find(strtok(NULL, " "));

				mc->registered = atoi(strtok(NULL, " "));
				mc->used = atoi(strtok(NULL, " "));
				mc->flags = atoi(strtok(NULL, " "));

				mc->mlock_on = atoi(strtok(NULL, " "));
				mc->mlock_off = atoi(strtok(NULL, " "));
				mc->mlock_limit = atoi(strtok(NULL, " "));

				if ((s = strtok(NULL, " ")))
				{
					strip(s);
					if (*s != '\0' && *s != ':' && !strchr(s, ','))
						mc->mlock_key = sstrdup(s);
				}

				if (versn < 5 && config_options.join_chans)
					mc->flags |= MC_GUARD;
			}
		}
		else if (!strcmp("MD", item))
		{
			/* Metadata entry */
			char *type = strtok(NULL, " ");
			char *name = strtok(NULL, " ");
			char *property = strtok(NULL, " ");
			char *value = strtok(NULL, "");

			if (!type || !name || !property || !value)
				continue;

			strip(value);

			if (type[0] == 'U')
			{
				mu = myuser_find(name);
				if (mu != NULL)
					metadata_add(mu, property, value);
			}
			else if (type[0] == 'C')
			{
				mc = mychan_find(name);
				if (mc != NULL)
					metadata_add(mc, property, value);
			}
			else if (type[0] == 'A')
			{
				chanacs_t *ca;
				char *mask;

				mask = strrchr(name, ':');
				if (mask != NULL)
				{
					*mask++ = '\0';
					ca = chanacs_find_by_mask(mychan_find(name), mask, CA_NONE);
					if (ca != NULL)
						metadata_add(ca, property, value);
				}
			}
			else if (type[0] == 'N')
			{
				mun = myuser_name_find(name);
				if (mun != NULL)
					metadata_add(mun, property, value);
			}
			else
				slog(LG_DEBUG, "db_load(): unknown metadata type %s", type);
		}
		else if (!strcmp("UR", item))
		{
			/* Channel URLs (obsolete) */
			char *chan, *url;

			chan = strtok(NULL, " ");
			url = strtok(NULL, " ");

			strip(url);

			if (chan && url)
			{
				mc = mychan_find(chan);

				if (mc)
					metadata_add(mc, "url", url);
			}
		}
		else if (!strcmp("EM", item))
		{
			/* Channel entry messages (obsolete) */
			char *chan, *message;

			chan = strtok(NULL, " ");
			message = strtok(NULL, "");

			strip(message);

			if (chan && message)
			{
				mc = mychan_find(chan);

				if (mc)
					metadata_add(mc, "private:entrymsg", message);
			}
		}
		else if (!strcmp("CA", item))
		{
			/* chanacs */
			chanacs_t *ca;
			char *cachan, *causer;

			cachan = strtok(NULL, " ");
			causer = strtok(NULL, " ");

			if (cachan && causer)
			{
				mc = mychan_find(cachan);
				mu = myuser_find(causer);

				if (mc == NULL || (mu == NULL && !validhostmask(causer)))
				{
					slog(LG_ERROR, "db_load(): invalid chanacs (line %d)", linecnt);
					continue;
				}

				cain++;

				if (versn >= DB_ATHEME)
				{
					unsigned int fl = flags_to_bitmask(strtok(NULL, " "), 0);
					const char *tsstr;
					time_t ts = 0;

					/* Compatibility with oldworld Atheme db's. --nenolod */
					/* arbitrary cutoff to avoid touching newer +voOt entries -- jilles */
					if (fl == OLD_CA_AOP && versn < 4)
						fl = CA_AOP_DEF;

					/* 
					 * If the database revision is version 6 or newer, CA entries are
					 * timestamped... otherwise we use 0 as the last modified TS
					 *    --nenolod/jilles
					 */
					tsstr = strtok(NULL, " ");
					if (tsstr != NULL)
						ts = atoi(tsstr);

					/* previous to CA_ACLVIEW, everyone could view
					 * access lists. If they aren't AKICKed, upgrade
					 * them. This keeps us from breaking old XOPs.
					 */
					if (versn < 4)
						if (!(fl & CA_AKICK))
							fl |= CA_ACLVIEW;

					if (their_ca_all == 0)
					{
						their_ca_all = CA_VOICE | CA_AUTOVOICE | CA_OP | CA_AUTOOP | CA_TOPIC | CA_SET | CA_REMOVE | CA_INVITE | CA_RECOVER | CA_FLAGS | CA_HALFOP | CA_AUTOHALFOP | CA_ACLVIEW | CA_AKICK;
						slog(LG_INFO, "db_load(): old database, making up flags %s", bitmask_to_flags(~their_ca_all & ca_all));
					}

					/* Grant +h if they have +o,
					 * the file does not have +h enabled
					 * and we currently have +h enabled.
					 * This preserves AOP, SOP and +*.
					 */
					if (fl & CA_OP && !(their_ca_all & CA_HALFOP) && ca_all & CA_HALFOP)
						fl |= CA_HALFOP;

					/* Set new-style founder flag */
					if (founder != NULL && mu == founder && !(their_ca_all & CA_FOUNDER))
						fl |= CA_FOUNDER;

					/* Set new-style +q and +a flags */
					if (fl & CA_SET && fl & CA_OP && !(their_ca_all & CA_USEPROTECT) && ca_all & CA_USEPROTECT)
						fl |= CA_USEPROTECT;
					if (fl & CA_FOUNDER && !(their_ca_all & CA_USEOWNER) && ca_all & CA_USEOWNER)
						fl |= CA_USEOWNER;

					if ((!mu) && (validhostmask(causer)))
						ca = chanacs_add_host(mc, causer, fl, ts, NULL);
					else
						ca = chanacs_add(mc, entity(mu), fl, ts, NULL);
				}
				else if (versn == DB_SHRIKE)	/* DB_SHRIKE */
				{
					unsigned int fl = atol(strtok(NULL, " "));
					unsigned int fl2 = 0x0;
					time_t ts = CURRTIME;

					switch (fl)
					{
					  case SHRIKE_CA_VOP:
						  fl2 = get_template_flags(mc, "VOP");
						  break;
					  case SHRIKE_CA_AOP:
						  fl2 = get_template_flags(mc, "AOP");
						  break;
					  case SHRIKE_CA_SOP:
						  fl2 = get_template_flags(mc, "SOP");
						  break;
					  case SHRIKE_CA_SUCCESSOR:
						  fl2 = CA_SUCCESSOR_0;
						  break;
					  case SHRIKE_CA_FOUNDER:
						  fl2 = CA_FOUNDER_0;
						  break;
					}

					if ((!mu) && (validhostmask(causer)))
						ca = chanacs_add_host(mc, causer, fl2, ts, NULL);
					else
						ca = chanacs_add(mc, entity(mu), fl2, ts, NULL);
				}
			}
		}
		else if (!strcmp("SI", item))
		{
				/* Services ignores */
			char *mask, *setby, *reason, *tmp;
			time_t settime;

			mask = strtok(NULL, " ");
                        tmp = strtok(NULL, " ");
			settime = atol(tmp);
			setby = strtok(NULL, " ");
			reason = strtok(NULL, "");

			strip(reason);

			svsignore = svsignore_add(mask, reason);
			svsignore->settime = settime;
			svsignore->setby = sstrdup(setby);

		}
		else if (!strcmp("KID", item))
		{
			/* unique kline id */
			char *id = strtok(NULL, " ");
			me.kline_id = atol(id);
		}
		else if (!strcmp("KL", item))
		{
			/* klines */
			char *user, *host, *reason, *setby, *tmp;
			time_t settime;
			long duration;

			user = strtok(NULL, " ");
			host = strtok(NULL, " ");
			tmp = strtok(NULL, " ");
			duration = atol(tmp);
			tmp = strtok(NULL, " ");
			settime = atol(tmp);
			setby = strtok(NULL, " ");
			reason = strtok(NULL, "");

			strip(reason);

			k = kline_add(user, host, reason, duration, setby);
			k->settime = settime;
			/* XXX this is not nice, oh well -- jilles */
			k->expires = k->settime + k->duration;

			kin++;
		}
		else if (!strcmp("XID", item))
		{
			/* unique xline id */
			char *id = strtok(NULL, " ");
			me.xline_id = atol(id);
		}
		else if (!strcmp("XL", item))
		{
			char *realname, *reason, *setby, *tmp;
			time_t settime;
			long duration;

			realname = strtok(NULL, " ");
			tmp = strtok(NULL, " ");
			duration = atol(tmp);
			tmp = strtok(NULL, " ");
			settime = atol(tmp);
			setby = strtok(NULL, " ");
			reason = strtok(NULL, "");

			strip(reason);

			x = xline_add(realname, reason, duration, setby);
			x->settime = settime;

			/* XXX this is not nice, oh well -- jilles */
			x->expires = x->settime + x->duration;

			xin++;
		}
		else if (!strcmp("QID", item))
		{
			/* unique qline id */
			char *id = strtok(NULL, " ");
			me.qline_id = atol(id);
		}
		else if (!strcmp("QL", item))
		{
			char *mask, *reason, *setby, *tmp;
			time_t settime;
			long duration;

			mask = strtok(NULL, " ");
			tmp = strtok(NULL, " ");
			duration = atol(tmp);
			tmp = strtok(NULL, " ");
			settime = atol(tmp);
			setby = strtok(NULL, " ");
			reason = strtok(NULL, "");

			strip(reason);

			q = qline_add(mask, reason, duration, setby);
			q->settime = settime;

			/* XXX this is not nice, oh well -- jilles */
			q->expires = q->settime + q->duration;

			qin++;
		}
		else if (!strcmp("DE", item))
		{
			/* end */
			i = atoi(strtok(NULL, " "));
			if (i != muin)
				slog(LG_ERROR, "db_load(): got %d myusers; expected %d", muin, i);

			i = atoi(strtok(NULL, " "));
			if (i != mcin)
				slog(LG_ERROR, "db_load(): got %d mychans; expected %d", mcin, i);

			i = atoi(strtok(NULL, " "));
			if (i != cain)
				slog(LG_ERROR, "db_load(): got %d chanacs; expected %d", cain, i);

			if ((s = strtok(NULL, " ")))
				if ((i = atoi(s)) != kin)
					slog(LG_ERROR, "db_load(): got %d klines; expected %d", kin, i);

			if ((s = strtok(NULL, " ")))
				if ((i = atoi(s)) != xin)
					slog(LG_ERROR, "db_load(): got %d xlines; expected %d", xin, i);

			if ((s = strtok(NULL, " ")))
				if ((i = atoi(s)) != qin)
					slog(LG_ERROR, "db_load(): got %d qlines; expected %d", qin, i);
		}
	}

	fclose(f);

	free(buf);

	slog(LG_DEBUG, "db_load(): ------------------------- done -------------------------");
	db_save(NULL);

	slog(LG_INFO, "Your database has been converted to the new OpenSEX format automatically.");
	slog(LG_INFO, "You must now change the backend module in the config file to ensure that the OpenSEX database is loaded.");
	slog(LG_INFO, "Exiting...");

	exit(EXIT_SUCCESS);
}

void _modinit(module_t *m)
{
	MODULE_TRY_REQUEST_DEPENDENCY(m, "backend/opensex");

	m->mflags = MODTYPE_CORE;

	db_load = &flatfile_db_load;

	backend_loaded = true;
}

/* vim:cinoptions=>s,e0,n0,f0,{0,}0,^0,=s,ps,t0,c3,+s,(2s,us,)20,*30,gs,hs
 * vim:ts=8
 * vim:sw=8
 * vim:noexpandtab
 */
