#include "stdinc.h"

#include "9.h"

enum {
	NUserHash	= 1009,
};

typedef struct Ubox Ubox;
typedef struct User User;

struct User {
	char*	uid;
	char*	uname;
	char*	leader;
	char**	group;
	int	ngroup;

	User*	next;			/* */
	User*	ihash;			/* lookup by .uid */
	User*	nhash;			/* lookup by .uname */
};

#pragma varargck type "U"   User*

struct Ubox {
	User*	head;
	User*	tail;
	int	nuser;
	int	len;

	User*	ihash[NUserHash];	/* lookup by .uid */
	User*	nhash[NUserHash];	/* lookup by .uname */
};

static struct {
	RWLock	lock;

	Ubox*	box;
} ubox;

static char usersDefault[] = {
	"adm:adm:adm:sys\n"
	"none:none::\n"
	"noworld:noworld::\n"
	"sys:sys::glenda\n"
	"glenda:glenda:glenda:\n"
};

static char* usersMandatory[] = {
	"adm",
	"none",
	"noworld",
	"sys",
	nil,
};

char* uidadm = "adm";
char* unamenone = "none";
char* uidnoworld = "noworld";

static u32int
userHash(char* s)
{
	uchar *p;
	u32int hash;

	hash = 0;
	for(p = (uchar*)s; *p != '\0'; p++)
		hash = hash*7 + *p;

	return hash % NUserHash;
}

static User*
_userByUid(Ubox* box, char* uid)
{
	User *u;

	if(box != nil){
		for(u = box->ihash[userHash(uid)]; u != nil; u = u->ihash){
			if(strcmp(u->uid, uid) == 0)
				return u;
		}
	}
	werrstr("uname: uid '%s' not found", uid);
	return nil;
}

char*
unameByUid(char* uid)
{
	User *u;
	char *uname;

	rlock(&ubox.lock);
	if((u = _userByUid(ubox.box, uid)) == nil){
		runlock(&ubox.lock);
		return nil;
	}
	uname = vtstrdup(u->uname);
	runlock(&ubox.lock);

	return uname;
}

static User*
_userByUname(Ubox* box, char* uname)
{
	User *u;

	if(box != nil){
		for(u = box->nhash[userHash(uname)]; u != nil; u = u->nhash){
			if(strcmp(u->uname, uname) == 0)
				return u;
		}
	}
	werrstr("uname: uname '%s' not found", uname);
	return nil;
}

char*
uidByUname(char* uname)
{
	User *u;
	char *uid;

	rlock(&ubox.lock);
	if((u = _userByUname(ubox.box, uname)) == nil){
		runlock(&ubox.lock);
		return nil;
	}
	uid = vtstrdup(u->uid);
	runlock(&ubox.lock);

	return uid;
}

static int
_groupMember(Ubox* box, char* group, char* member, int whenNoGroup)
{
	int i;
	User *g, *m;

	/*
	 * Is 'member' a member of 'group'?
	 * Note that 'group' is a 'uid' and not a 'uname'.
	 * A 'member' is automatically in their own group.
	 */
	if((g = _userByUid(box, group)) == nil)
		return whenNoGroup;
	if((m = _userByUname(box, member)) == nil)
		return 0;
	if(m == g)
		return 1;
	for(i = 0; i < g->ngroup; i++){
		if(strcmp(g->group[i], member) == 0)
			return 1;
	}
	return 0;
}

int
groupWriteMember(char* uname)
{
	int ret;

	/*
	 * If there is a ``write'' group, then only its members can write
	 * to the file system, no matter what the permission bits say.
	 *
	 * To users not in the ``write'' group, the file system appears
	 * read only.  This is used to serve sources.cs.bell-labs.com
	 * to the world.
	 *
	 * Note that if there is no ``write'' group, then this routine
	 * makes it look like everyone is a member -- the opposite
	 * of what groupMember does.
	 *
	 * We use this for sources.cs.bell-labs.com.
	 * If this slows things down too much on systems that don't
	 * use this functionality, we could cache the write group lookup.
	 */

	rlock(&ubox.lock);
	ret = _groupMember(ubox.box, "write", uname, 1);
	runlock(&ubox.lock);
	return ret;
}

static int
_groupRemMember(Ubox* box, User* g, char* member)
{
	int i;

	if(_userByUname(box, member) == nil)
		return 0;

	for(i = 0; i < g->ngroup; i++){
		if(strcmp(g->group[i], member) == 0)
			break;
	}
	if(i >= g->ngroup){
		if(strcmp(g->uname, member) == 0)
			werrstr("uname: '%s' always in own group", member);
		else
			werrstr("uname: '%s' not in group '%s'",
				member, g->uname);
		return 0;
	}

	vtfree(g->group[i]);

	box->len -= strlen(member);
	if(g->ngroup > 1)
		box->len--;
	g->ngroup--;
	switch(g->ngroup){
	case 0:
		vtfree(g->group);
		g->group = nil;
		break;
	default:
		for(; i < g->ngroup; i++)
			g->group[i] = g->group[i+1];
		g->group[i] = nil;		/* prevent accidents */
		g->group = vtrealloc(g->group, g->ngroup * sizeof(char*));
		break;
	}

	return 1;
}

static int
_groupAddMember(Ubox* box, User* g, char* member)
{
	User *u;

	if((u = _userByUname(box, member)) == nil)
		return 0;
	if(_groupMember(box, g->uid, u->uname, 0)){
		if(strcmp(g->uname, member) == 0)
			werrstr("uname: '%s' always in own group", member);
		else
			werrstr("uname: '%s' already in group '%s'",
				member, g->uname);
		return 0;
	}

	g->group = vtrealloc(g->group, (g->ngroup+1)*sizeof(char*));
	g->group[g->ngroup] = vtstrdup(member);
	box->len += strlen(member);
	g->ngroup++;
	if(g->ngroup > 1)
		box->len++;

	return 1;
}

int
groupMember(char* group, char* member)
{
	int r;

	if(group == nil)
		return 0;

	rlock(&ubox.lock);
	r = _groupMember(ubox.box, group, member, 0);
	runlock(&ubox.lock);

	return r;
}

int
groupLeader(char* group, char* member)
{
	int r;
	User *g;

	/*
	 * Is 'member' the leader of 'group'?
	 * Note that 'group' is a 'uid' and not a 'uname'.
	 * Uname 'none' cannot be a group leader.
	 */
	if(strcmp(member, unamenone) == 0 || group == nil)
		return 0;

	rlock(&ubox.lock);
	if((g = _userByUid(ubox.box, group)) == nil){
		runlock(&ubox.lock);
		return 0;
	}
	if(g->leader != nil){
		if(strcmp(g->leader, member) == 0){
			runlock(&ubox.lock);
			return 1;
		}
		r = 0;
	}
	else
		r = _groupMember(ubox.box, group, member, 0);
	runlock(&ubox.lock);

	return r;
}

static void
userFree(User* u)
{
	int i;

	vtfree(u->uid);
	vtfree(u->uname);
	if(u->leader != nil)
		vtfree(u->leader);
	if(u->ngroup){
		for(i = 0; i < u->ngroup; i++)
			vtfree(u->group[i]);
		vtfree(u->group);
	}
	vtfree(u);
}

static User*
userAlloc(char* uid, char* uname)
{
	User *u;

	u = vtmallocz(sizeof(User));
	u->uid = vtstrdup(uid);
	u->uname = vtstrdup(uname);

	return u;
}

int
validUserName(char* name)
{
	Rune *r;
#ifdef PLAN9PORT
	static Rune invalid[] = {'#', ':', ',', '(', ')', '\0'};
#else
	static Rune invalid[] = L"#:,()";
#endif

	for(r = invalid; *r != '\0'; r++){
		if(utfrune(name, *r))
			return 0;
	}
	return 1;
}

static int
userFmt(Fmt* fmt)
{
	User *u;
	int i, r;

	u = va_arg(fmt->args, User*);

	r = fmtprint(fmt, "%s:%s:", u->uid, u->uname);
	if(u->leader != nil)
		r += fmtprint(fmt, u->leader);
	r += fmtprint(fmt, ":");
	if(u->ngroup){
		r += fmtprint(fmt, u->group[0]);
		for(i = 1; i < u->ngroup; i++)
			r += fmtprint(fmt, ",%s", u->group[i]);
	}

	return r;
}

static int
usersFileWrite(Ubox* box)
{
	Fs *fs;
	User *u;
	int i, r;
	Fsys *fsys;
	char *p, *q, *s;
	File *dir, *file;

	if((fsys = fsysGet("main")) == nil)
		return 0;
	fsysFsRlock(fsys);
	fs = fsysGetFs(fsys);

	/*
	 * BUG:
	 * 	the owner/group/permissions need to be thought out.
	 */
	r = 0;
	if((dir = fileOpen(fs, "/active")) == nil)
		goto tidy0;
	if((file = fileWalk(dir, uidadm)) == nil)
		file = fileCreate(dir, uidadm, ModeDir|0775, uidadm);
	fileDecRef(dir);
	if(file == nil)
		goto tidy;
	dir = file;
	if((file = fileWalk(dir, "users")) == nil)
		file = fileCreate(dir, "users", 0664, uidadm);
	fileDecRef(dir);
	if(file == nil)
		goto tidy;
	if(!fileTruncate(file, uidadm))
		goto tidy;

	p = s = vtmalloc(box->len+1);
	q = p + box->len+1;
	for(u = box->head; u != nil; u = u->next){
		p += snprint(p, q-p, "%s:%s:", u->uid, u->uname);
		if(u->leader != nil)
			p+= snprint(p, q-p, u->leader);
		p += snprint(p, q-p, ":");
		if(u->ngroup){
			p += snprint(p, q-p, u->group[0]);
			for(i = 1; i < u->ngroup; i++)
				p += snprint(p, q-p, ",%s", u->group[i]);
		}
		p += snprint(p, q-p, "\n");
	}
	r = fileWrite(file, s, box->len, 0, uidadm);
	vtfree(s);

tidy:
	if(file != nil)
		fileDecRef(file);
tidy0:
	fsysFsRUnlock(fsys);
	fsysPut(fsys);

	return r;
}

static void
uboxRemUser(Ubox* box, User *u)
{
	User **h, *up;

	h = &box->ihash[userHash(u->uid)];
	for(up = *h; up != nil && up != u; up = up->ihash)
		h = &up->ihash;
	assert(up == u);
	*h = up->ihash;
	box->len -= strlen(u->uid);

	h = &box->nhash[userHash(u->uname)];
	for(up = *h; up != nil && up != u; up = up->nhash)
		h = &up->nhash;
	assert(up == u);
	*h = up->nhash;
	box->len -= strlen(u->uname);

	h = &box->head;
	for(up = *h; up != nil && strcmp(up->uid, u->uid) != 0; up = up->next)
		h = &up->next;
	assert(up == u);
	*h = u->next;
	u->next = nil;

	box->len -= 4;
	box->nuser--;
}

static void
uboxAddUser(Ubox* box, User* u)
{
	User **h, *up;

	h = &box->ihash[userHash(u->uid)];
	u->ihash = *h;
	*h = u;
	box->len += strlen(u->uid);

	h = &box->nhash[userHash(u->uname)];
	u->nhash = *h;
	*h = u;
	box->len += strlen(u->uname);

	h = &box->head;
	for(up = *h; up != nil && strcmp(up->uid, u->uid) < 0; up = up->next)
		h = &up->next;
	u->next = *h;
	*h = u;

	box->len += 4;
	box->nuser++;
}

static void
uboxDump(Ubox* box)
{
	User* u;

	consPrint("nuser %d len = %d\n", box->nuser, box->len);

	for(u = box->head; u != nil; u = u->next)
		consPrint("%U\n", u);
}

static void
uboxFree(Ubox* box)
{
	User *next, *u;

	for(u = box->head; u != nil; u = next){
		next = u->next;
		userFree(u);
	}
	vtfree(box);
}

static int
uboxInit(char* users, int len)
{
	User *g, *u;
	Ubox *box, *obox;
	int blank, comment, i, nline, nuser;
	char *buf, *f[5], **line, *p, *q, *s;

	/*
	 * Strip out whitespace and comments.
	 * Note that comments are pointless, they disappear
	 * when the server writes the database back out.
	 */
	blank = 1;
	comment = nline = 0;

	s = p = buf = vtmalloc(len+1);
	for(q = users; *q != '\0'; q++){
		if(*q == '\r' || *q == '\t' || *q == ' ')
			continue;
		if(*q == '\n'){
			if(!blank){
				if(p != s){
					*p++ = '\n';
					nline++;
					s = p;
				}
				blank = 1;
			}
			comment = 0;
			continue;
		}
		if(*q == '#')
			comment = 1;
		blank = 0;
		if(!comment)
			*p++ = *q;
	}
	*p = '\0';

	line = vtmallocz((nline+2)*sizeof(char*));
	if((i = gettokens(buf, line, nline+2, "\n")) != nline){
		fprint(2, "nline %d (%d) botch\n", nline, i);
		vtfree(line);
		vtfree(buf);
		return 0;
	}

	/*
	 * Everything is updated in a local Ubox until verified.
	 */
	box = vtmallocz(sizeof(Ubox));

	/*
	 * First pass - check format, check for duplicates
	 * and enter in hash buckets.
	 */
	nuser = 0;
	for(i = 0; i < nline; i++){
		s = vtstrdup(line[i]);
		if(getfields(s, f, nelem(f), 0, ":") != 4){
			fprint(2, "bad line '%s'\n", line[i]);
			vtfree(s);
			continue;
		}
		if(*f[0] == '\0' || *f[1] == '\0'){
			fprint(2, "bad line '%s'\n", line[i]);
			vtfree(s);
			continue;
		}
		if(!validUserName(f[0])){
			fprint(2, "invalid uid '%s'\n", f[0]);
			vtfree(s);
			continue;
		}
		if(_userByUid(box, f[0]) != nil){
			fprint(2, "duplicate uid '%s'\n", f[0]);
			vtfree(s);
			continue;
		}
		if(!validUserName(f[1])){
			fprint(2, "invalid uname '%s'\n", f[0]);
			vtfree(s);
			continue;
		}
		if(_userByUname(box, f[1]) != nil){
			fprint(2, "duplicate uname '%s'\n", f[1]);
			vtfree(s);
			continue;
		}

		u = userAlloc(f[0], f[1]);
		uboxAddUser(box, u);
		line[nuser] = line[i];
		nuser++;

		vtfree(s);
	}
	assert(box->nuser == nuser);

	/*
	 * Second pass - fill in leader and group information.
	 */
	for(i = 0; i < nuser; i++){
		s = vtstrdup(line[i]);
		getfields(s, f, nelem(f), 0, ":");

		assert(g = _userByUname(box, f[1]));
		if(*f[2] != '\0'){
			if((u = _userByUname(box, f[2])) == nil)
				g->leader = vtstrdup(g->uname);
			else
				g->leader = vtstrdup(u->uname);
			box->len += strlen(g->leader);
		}
		for(p = f[3]; p != nil; p = q){
			if((q = utfrune(p, L',')) != nil)
				*q++ = '\0';
			if(!_groupAddMember(box, g, p)){
				// print/log error here
			}
		}

		vtfree(s);
	}

	vtfree(line);
	vtfree(buf);

	for(i = 0; usersMandatory[i] != nil; i++){
		if((u = _userByUid(box, usersMandatory[i])) == nil){
			werrstr("user '%s' is mandatory", usersMandatory[i]);
			uboxFree(box);
			return 0;
		}
		if(strcmp(u->uid, u->uname) != 0){
			werrstr("uid/uname for user '%s' must match",
				usersMandatory[i]);
			uboxFree(box);
			return 0;
		}
	}

	wlock(&ubox.lock);
	obox = ubox.box;
	ubox.box = box;
	wunlock(&ubox.lock);

	if(obox != nil)
		uboxFree(obox);

	return 1;
}

int
usersFileRead(char* path)
{
	char *p;
	File *file;
	Fsys *fsys;
	int len, r;
	uvlong size;

	if((fsys = fsysGet("main")) == nil)
		return 0;
	fsysFsRlock(fsys);

	if(path == nil)
		path = "/active/adm/users";

	r = 0;
	if((file = fileOpen(fsysGetFs(fsys), path)) != nil){
		if(fileGetSize(file, &size)){
			len = size;
			p = vtmalloc(size+1);
			if(fileRead(file, p, len, 0) == len){
				p[len] = '\0';
				r = uboxInit(p, len);
			}
		}
		fileDecRef(file);
	}

	fsysFsRUnlock(fsys);
	fsysPut(fsys);

	return r;
}

static int
cmdUname(int argc, char* argv[])
{
	User *u, *up;
	int d, dflag, i, r;
	char *p, *uid, *uname;
	char *createfmt = "fsys main create /active/usr/%s %s %s d775";
	char *usage = "usage: uname [-d] uname [uid|:uid|%%newname|=leader|+member|-member]";

	dflag = 0;

	ARGBEGIN{
	default:
		return cliError(usage);
	case 'd':
		dflag = 1;
		break;
	}ARGEND

	if(argc < 1){
		if(!dflag)
			return cliError(usage);
		rlock(&ubox.lock);
		uboxDump(ubox.box);
		runlock(&ubox.lock);
		return 1;
	}

	uname = argv[0];
	argc--; argv++;

	if(argc == 0){
		rlock(&ubox.lock);
		if((u = _userByUname(ubox.box, uname)) == nil){
			runlock(&ubox.lock);
			return 0;
		}
		consPrint("\t%U\n", u);
		runlock(&ubox.lock);
		return 1;
	}

	wlock(&ubox.lock);
	u = _userByUname(ubox.box, uname);
	while(argc--){
		if(argv[0][0] == '%'){
			if(u == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			p = &argv[0][1];
			if((up = _userByUname(ubox.box, p)) != nil){
				werrstr("uname: uname '%s' already exists",
					up->uname);
				wunlock(&ubox.lock);
				return 0;
			}
			for(i = 0; usersMandatory[i] != nil; i++){
				if(strcmp(usersMandatory[i], uname) != 0)
					continue;
				werrstr("uname: uname '%s' is mandatory",
					uname);
				wunlock(&ubox.lock);
				return 0;
			}

			d = strlen(p) - strlen(u->uname);
			for(up = ubox.box->head; up != nil; up = up->next){
				if(up->leader != nil){
					if(strcmp(up->leader, u->uname) == 0){
						vtfree(up->leader);
						up->leader = vtstrdup(p);
						ubox.box->len += d;
					}
				}
				for(i = 0; i < up->ngroup; i++){
					if(strcmp(up->group[i], u->uname) != 0)
						continue;
					vtfree(up->group[i]);
					up->group[i] = vtstrdup(p);
					ubox.box->len += d;
					break;
				}
			}

			uboxRemUser(ubox.box, u);
			vtfree(u->uname);
			u->uname = vtstrdup(p);
			uboxAddUser(ubox.box, u);
		}
		else if(argv[0][0] == '='){
			if(u == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			if((up = _userByUname(ubox.box, &argv[0][1])) == nil){
				if(argv[0][1] != '\0'){
					wunlock(&ubox.lock);
					return 0;
				}
			}
			if(u->leader != nil){
				ubox.box->len -= strlen(u->leader);
				vtfree(u->leader);
				u->leader = nil;
			}
			if(up != nil){
				u->leader = vtstrdup(up->uname);
				ubox.box->len += strlen(u->leader);
			}
		}
		else if(argv[0][0] == '+'){
			if(u == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			if((up = _userByUname(ubox.box, &argv[0][1])) == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			if(!_groupAddMember(ubox.box, u, up->uname)){
				wunlock(&ubox.lock);
				return 0;
			}
		}
		else if(argv[0][0] == '-'){
			if(u == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			if((up = _userByUname(ubox.box, &argv[0][1])) == nil){
				wunlock(&ubox.lock);
				return 0;
			}
			if(!_groupRemMember(ubox.box, u, up->uname)){
				wunlock(&ubox.lock);
				return 0;
			}
		}
		else{
			if(u != nil){
				werrstr("uname: uname '%s' already exists",
					u->uname);
				wunlock(&ubox.lock);
				return 0;
			}

			uid = argv[0];
			if(*uid == ':')
				uid++;
			if((u = _userByUid(ubox.box, uid)) != nil){
				werrstr("uname: uid '%s' already exists",
					u->uid);
				wunlock(&ubox.lock);
				return 0;
			}

			u = userAlloc(uid, uname);
			uboxAddUser(ubox.box, u);
			if(argv[0][0] != ':'){
				// should have an option for the mode and gid
				p = smprint(createfmt, uname, uname, uname);
				r = cliExec(p);
				vtfree(p);
				if(r == 0){
					wunlock(&ubox.lock);
					return 0;
				}
			}
		}
		argv++;
	}

	if(usersFileWrite(ubox.box) == 0){
		wunlock(&ubox.lock);
		return 0;
	}
	if(dflag)
		uboxDump(ubox.box);
	wunlock(&ubox.lock);

	return 1;
}

static int
cmdUsers(int argc, char* argv[])
{
	Ubox *box;
	int dflag, r, wflag;
	char *file;
	char *usage = "usage: users [-d | -r file] [-w]";

	dflag = wflag = 0;
	file = nil;

	ARGBEGIN{
	default:
		return cliError(usage);
	case 'd':
		dflag = 1;
		break;
	case 'r':
		file = ARGF();
		if(file == nil)
			return cliError(usage);
		break;
	case 'w':
		wflag = 1;
		break;
	}ARGEND

	if(argc)
		return cliError(usage);

	if(dflag && file)
		return cliError("cannot use -d and -r together");

	if(dflag)
		uboxInit(usersDefault, sizeof(usersDefault));
	else if(file){
		if(usersFileRead(file) == 0)
			return 0;
	}

	rlock(&ubox.lock);
	box = ubox.box;
	consPrint("\tnuser %d len %d\n", box->nuser, box->len);

	r = 1;
	if(wflag)
		r = usersFileWrite(box);
	runlock(&ubox.lock);
	return r;
}

int
usersInit(void)
{
	fmtinstall('U', userFmt);

	uboxInit(usersDefault, sizeof(usersDefault));

	cliAddCmd("users", cmdUsers);
	cliAddCmd("uname", cmdUname);

	return 1;
}
