-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
local struct srv
local struct cfgcache {
	secret: pstring
	pol_sec: secmode.t
	pol_reg: bool
	pol_autoherald: bool
	credmgd: bool
	maxupsz: intptr
	poolinitsz: intptr
	instance: pstring
	overlord: &srv
	ui_cue_staff: pstring
	ui_cue_founder: pstring
	ui_hue: uint16
	nranks: uint16
	maxinvites: uint16
	master: uint64

	usrdef_pol_follow: pstring
	usrdef_pol_follow_req: pstring
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
	pool: lib.mem.pool
}

terra cfgcache:free() -- :/
	self.secret:free()
	self.instance:free()
	self.ui_cue_staff:free()
	self.ui_cue_founder:free()
	self.usrdef_pol_follow:free()
	self.usrdef_pol_follow_req:free()
end

terra srv:post_enum_author_uid(uid: uint64, r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
	var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then
			var lst = src:post_enum_author_uid(uid,r)
			all:assure(all.sz + lst.ct)
			for j=0, lst.ct do all:push(lst.ptr[j]) end
			lst:free()
		end
	end
	return all
end

local function deftlfetch(fnname, ...)
	local args = {}
	for i,ty in ipairs{...} do args[#args + 1] = symbol(ty) end
	args[#args + 1] = symbol(lib.store.range)
	fnname = 'timeline_' .. fnname
	srv.methods[fnname] = terra(self: &srv, [args]): lib.mem.vec(lib.mem.ptr(lib.store.post))
		var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
		for i=0,self.sources.ct do var src = self.sources.ptr + i
			if src.handle ~= nil and src.backend.[fnname] ~= nil then
				var lst = src:[fnname]([args])
				all:assure(all.sz + lst.ct)
				for j=0, lst.ct do all:push(lst.ptr[j]) end
				lst:free()
			end
		end
		return all
	end
end

deftlfetch('instance_fetch')
deftlfetch('actor_fetch_uid', uint64)
deftlfetch('circle_fetch', uint64)

srv.metamethods.__methodmissing = macro(function(meth, self, ...)
	local primary, ptr, stat, simple, oid = 0,1,2,3,4
	local tk, rt = primary
	local expr = {...}
	for _,f in pairs(lib.store.backend.entries) do
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then
			rt = ft.type.returntype
			if rt == bool then tk = simple
			elseif rt.type == 'integer' then tk = oid
			elseif rt.stat_basetype then tk = stat
			elseif rt.ptr_basetype then tk = ptr end
			break
		end
	end
	
	local r = symbol(rt)
	local succ = label()
	if tk == primary then
		return quote
			var [r]
			for i=0,self.sources.ct do var src = self.sources.ptr + i
				if src.handle ~= nil and src.backend.[meth] ~= nil then
					r = src:[meth]([expr])
					goto [succ]
				end
			end
			lib.bail(['no active backends provide critical capability ' .. meth .. '!'])
			::[succ]::;
		in r end
	else local ok, empty
		if tk == ptr then
			ok = `r.ptr ~= nil
			empty = `[rt]{ptr=nil,ct=0}
		elseif tk == stat then
			ok = `r.ok == true
			empty = `[rt]{ok=false,error=1}
		elseif tk == simple then
			ok = `r == true
			empty = `false
		elseif tk == oid then
			ok = `r ~= 0
			empty = `0
		end
		return quote
			var [r] = empty
			for i=0,self.sources.ct do var src = self.sources.ptr + i
				if src.handle ~= nil and src.backend.[meth] ~= nil then
					r = src:[meth]([expr])
					if [ok] then break
						else r = empty end
				end
			end
		in r end
	end
end)

terra lib.store.post:publish(s: &srv)
	self:comp()
	self.posted = lib.osclock.time(nil)
	self.discovered = self.posted
	self.chgcount = 0
	self.edited = 0
	self.id = s:post_create(self)
	return self.id
end

local struct convo {
	srv: &srv
	con: &lib.net.mg_connection
	msg: &lib.net.mg_http_message
	aid: uint64 -- 0 if logged out
	aid_issue: lib.store.timepoint
	who: &lib.store.actor -- who we're logged in as, if aid ~= 0
	peer: lib.store.inet
	reqtype: lib.http.mime.t -- negotiated content type
	method: lib.http.method.t
	live_last: lib.store.timepoint
	uploads: lib.mem.vec(lib.http.upload)
	body: lib.str.t
-- cache
	ui_hue: uint16
	navbar: lib.mem.ptr(int8)
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
	varbuf: lib.mem.ptr(int8)
	vbofs: &int8
}

struct convo.page {
	title: pstring
	body: pstring
	class: pstring
	cache: bool
}

local usrdefs = {
	str = {
		['acl-follow'    ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
		['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
	};
}

terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
	var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
	if not set then
		[(function()
			local q = quote return pstring.null() end
			for key, dfl in pairs(usrdefs.str) do
				local rv
				if dfl.cfgfld then
					rv = quote
						var cf = self.srv.cfg.[dfl.cfgfld]
					in terralib.select(not cf, pstring([dfl.fallback]), cf) end
				elseif dfl.lit then rv = dfl.lit end
				q = quote
					if setting:cmp([key]) then return [rv] else [q] end
				end
			end
			return q
		end)()]
	else return set end
end

-- this is unfortunately necessary to work around a terra bug
-- it can't seem to handle forward-declarations of structs in C

local getpeer
do local struct strucheader {
		next: &lib.net.mg_connection
		mgr: &lib.net.mg_mgr
		peer: lib.net.mg_addr
	}
	terra getpeer(con: &lib.net.mg_connection)
		return [&strucheader](con).peer
	end
end

terra convo:uid2actor_live(uid: uint64)
	var actor = self.srv:actor_fetch_uid(uid)
	if actor:ref() then
		if self.aid ~= 0 and self.who.id ~= uid then
			actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid)
		else -- defensive branch
			actor(0).relationship = lib.store.relationship {
				agent = 0, patient = uid;
				rel   = [lib.store.relation.null],
				recip = [lib.store.relation.null],
			}
		end
	end
	return actor
end

terra convo:uid2actor(uid: uint64)
	var actor: &lib.store.actor = nil
	for j = 0, self.actorcache.top do
		if uid == self.actorcache(j).ptr.id then
			actor = self.actorcache(j).ptr
			break
		end
	end
	if actor == nil then
		actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr
	end
	return actor
end

terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header))
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
		attr = '';
	}
	var attrbuf: int8[32]
	if self.aid ~= 0 and self.ui_hue ~= 323 then
		var hdecbuf: int8[21]
		var hdec = lib.math.decstr(self.ui_hue, &hdecbuf[20])
		lib.str.cpy(&attrbuf[0], ' style="--hue:')
		lib.str.cpy(&attrbuf[14], hdec)
		var len = &hdecbuf[20] - hdec 
		lib.str.cpy(&attrbuf[14] + len, '"')
		doc.attr = &attrbuf[0]
	end

	if self.method == [lib.http.method.head]
		then doc:head(self.con,code,hdrs)
		else doc:send(self.con,code,hdrs)
	end
end

terra convo:statpage(code: uint16, pg: convo.page)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)
	self:rawpage(code,pg, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0];
		ct = [hdrs.type.N] - lib.trn(pg.cache,1,0);
	})
end

terra convo:livepage(pg: convo.page, lastup: lib.store.timepoint)
	var nbuf: int8[21]
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' },
		lib.http.header {
			key = 'X-Live-Newest-Artifact';
			value = lib.math.decstr(lastup, &nbuf[20]);
		},
		lib.http.header { key = 'Content-Length', value = '0' }
	)
	if self.live_last ~= 0 and self.live_last == lastup then
		lib.net.mg_printf(self.con, 'HTTP/1.1 %s', lib.http.codestr(200))
		for i = 0, [hdrs.type.N] do
			lib.net.mg_printf(self.con, '%s: %s\r\n', hdrs[i].key, hdrs[i].value)
		end
		lib.net.mg_printf(self.con, '\r\n')
	else
		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
			ptr = &hdrs[0], ct = 3
		})
	end
end

terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end

terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
	-- chest wound. ultimately we need to compile a whitelist of safe mime
	-- types as part of mimelib, but that is no small task. for now, this
	-- will keep the patient from immediately bleeding out
	if mime:cmp('text/html') or
		mime:cmp('text/xml') or
		mime:cmp('application/xhtml+xml') or
		mime:cmp('application/vnd.wap.xhtml+xml')
	then -- danger will robinson
		mime = 'text/plain'
	elseif mime:cmp('application/x-shockwave-flash') then
		mime = 'application/octet-stream'
	end
	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2)
	lib.net.mg_send(self.con, data.ptr, data.ct)
	lib.net.mg_send(self.con, '\r\n', 2)
end

terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Location',     value = dest },
		lib.http.header { key = 'Set-Cookie',   value = cookie }
	)

	var body = data.view.docskel {
		instance = self.srv.cfg.instance.ptr;
		title = 'rerouting';
		body = 'you are being redirected';
		class = 'error';
		navlinks = '';
		attr = '';
	}

	body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] {
		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
	})
end

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end

terra convo:installkey(dest: rawstring, aid: uint64)
	var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
	do var p = &sesskey[0]
		p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
		p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p)
		lib.dbg('sending cookie ',{&sesskey[0],15})
		p = lib.str.ncpy(p, '; Path=/', 9)
	end
	self:reroute_cookie(dest, &sesskey[0])
end
 
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end

	var ti: lib.str.acc ti:compose('error :: ', title)
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>')
	var body = [convo.page] {
		title = ti:finalize();
		body = bo:finalize();
		class = 'error';
		cache = false;
	}

	self:statpage(code, body)

	body.title:free()
	body.body:free()
end

terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
	var conf = data.view.confirm {
		title = title;
		query = msg;
		cancel = cancel;
	}
	var ti: lib.str.acc ti:pcompose(&self.srv.pool,'confirm :: ', title)
	var body = conf:poolstr(&self.srv.pool) -- defer body:free()
	var cf = [convo.page] {
		title = ti:finalize();
		class = 'query';
		body = body; cache = false;
	}
	self:stdpage(cf)
	--cf.title:free()
end

terra convo:stra(sz: intptr) -- convenience function
	var s: lib.str.acc
	s:pool(&self.srv.pool,sz)
	return s
end

convo.methods.qstr = macro(function(self, ...) -- convenience string builder
	local exp = {...}
	return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
end)

convo.methods.assertpow = macro(function(self, pow)
	return quote
		var ok = true
		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
			ok = false
			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
		end
	in ok end
end)

local pstr2mg, mg2pstr
do -- aaaaaaaaaaaaaaaaaaaaaaaa
	mgstr = lib.util.find(lib.net.mg_http_message.entries, function(v)
		if v.field == 'body' or v[1] == 'body' then return v.type end
	end)
	terra pstr2mg(p: pstring): mgstr
		return mgstr { ptr = p.ptr, len = p.ct }
	end
	terra mg2pstr(m: mgstr): pstring
		return pstring { ptr = m.ptr, ct = m.len }
	end
end

-- CALL ONLY ONCE PER VAR
terra convo:postv_next(name: pstring, start: &pstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr
	end
	var conv = pstr2mg(@start)
	var o = lib.net.mg_http_get_var(
		&conv,
		name.ptr, self.vbofs,
		self.varbuf.ct - (self.vbofs - self.varbuf.ptr)
	)
	if o > 0 then
		start:advance(name.ct + o + 2)
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:postv(name: pstring)
	var start = mg2pstr(self.msg.body)
	return self:postv_next(name, &start)
end
terra convo:ppostv(name: pstring)
	var s,l = self:postv(name)
	return pstring { ptr = s, ct = l }
end
do
	local struct postiter { co: &convo where: pstring name: pstring }
	terra convo:eachpostv(name: pstring)
		return postiter { co = self, where = mg2pstr(self.msg.body), name = name } 
	end
	postiter.metamethods.__for = function(self, body)
		return quote
			while true do
				var str, len = self.co:postv_next(self.name, &self.where)
				if str == nil then break end
				[ body(`pstring {str, len}) ]
			end
		end
	end
end

terra convo:getv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = self.srv.pool:alloc(int8, self.msg.query.len + self.msg.body.len)
		self.vbofs = self.varbuf.ptr
	end
	var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr))
	if o > 0 then
		var r = self.vbofs
		self.vbofs = self.vbofs + o + 1
		@(self.vbofs - 1) = 0
		var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o})
		return norm.ptr, norm.ct
	else return nil, 0 end
end
terra convo:pgetv(name: rawstring)
	var s,l = self:getv(name)
	return pstring { ptr = s, ct = l }
end

local route = {} -- these are defined in route.t, as they need access to renderers
terra route.dispatch_http ::  {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}

local mimetypes = {
	{'html', 'text/html'};
	{'json', 'application/json'};
	{'mkdown', 'text/markdown'};
	{'text', 'text/plain'};
	{'ansi', 'text/x-ansi'};
}

local mimevar = symbol(lib.mem.ref(int8))
local mimeneg = `lib.http.mime.none

for i, t in ipairs(mimetypes) do
	local name, mime = t[1], t[2]
	mimeneg = quote
		var ret: lib.http.mime.t
		if lib.str.ncmp(mimevar.ptr, mime, lib.math.biggest(mimevar.ct, [#mime])) == 0 then
			ret = [lib.http.mime[name]]
		else ret = [mimeneg] end
	in ret end
end

local handle = {
	http = terra(con: &lib.net.mg_connection, event_kind: int, event: &opaque, userdata: &opaque)
		var server = [&srv](userdata)
		var mgpeer = getpeer(con)
		var peer = lib.store.inet { port = mgpeer.port; }
		if mgpeer.is_ip6 then peer.pv = 6 else peer.pv = 4 end
		if peer.pv == 6 then
			for i = 0, 16 do peer.v6[i] = mgpeer.ip6[i] end
		else -- v4
			@[&uint32](&peer.v4) = mgpeer.ip
		end
		-- the peer property is currently broken and there is precious
		-- little i can do about this -- it always reports a peer v4 IP
		-- of 0.0.0.0, altho the port seems to come through correctly.
		-- for now i'm leaving it as is, but note that netmask restrictions
		-- WILL NOT WORK until upstream gets its shit together. FIXME

		-- needs to check for an X-Forwarded-For header from nginx and
		-- use that instead of the peer iff peer is ::1/127.1 FIXME
		-- maybe also haproxy support?

		switch event_kind do
			case lib.net.MG_EV_HTTP_MSG then
				lib.dbg('routing HTTP request')
				var msg = [&lib.net.mg_http_message](event)
				var co = convo {
					con = con, srv = server, msg = msg;
					aid = 0, aid_issue = 0, who = nil;
					reqtype = lib.http.mime.none;
					peer = peer, live_last = 0;
				} co.varbuf.ptr = nil
				  co.navbar.ptr = nil
				  co.actorcache.top = 0
				  co.actorcache.cur = 0
				  co.ui_hue = server.cfg.ui_hue
				  co.body.ptr = msg.body.ptr co.body.ct = msg.body.len

				-- first, check for an accept header. if it's there, we need to
				-- iterate over the values and pick the highest-priority one
				do var acc = lib.http.findheader(msg, 'Accept')
					-- TODO handle q-value
					if acc ~= nil and acc.ptr ~= nil then
						var [mimevar] = [lib.mem.ref(int8)] { ptr = acc.ptr }
						var i = 0 while i < acc.ct do
							if acc.ptr[i] == @',' or acc.ptr[i] == @';' then
								mimevar.ct = (acc.ptr+i) - mimevar.ptr
								var t = [mimeneg]
								if t ~= lib.http.mime.none then
									co.reqtype = t
									goto foundtype
								end

								if acc.ptr[i] == @';' then -- fast-forward over q
									for j=i+1,acc.ct do i=j
										if acc.ptr[j] == @',' then break end
									end
								end
								
								while i < acc.ct and -- fast-forward over ws
									acc.ptr[i+1] == @' ' or
									acc.ptr[i+1] == @'\t'
								do i=i+1 end

								mimevar.ptr = acc.ptr + i + 1
							end
							i=i+1
						end
						if co.reqtype == lib.http.mime.none then
							mimevar.ct = acc.ct - (mimevar.ptr - acc.ptr)
							co.reqtype = [mimeneg]
							if co.reqtype == lib.http.mime.none then
								co.reqtype = lib.http.mime.html
							end
						end
					else co.reqtype = lib.http.mime.html end
				::foundtype::end

				-- we need to check if there's any cookies sent with the request,
				-- and if so, whether they contain any credentials. this will be
				-- used to set the auth parameters in the http conversation
				var cookies_p = lib.http.findheader(msg, 'Cookie')
				if cookies_p ~= nil and cookies_p.ptr ~= nil then
					var cookies = cookies_p.ptr
					var key = [lib.mem.ref(int8)] {ptr = cookies, ct = 0}
					var val = [lib.mem.ref(int8)] {ptr = nil, ct = 0}
					var i = 0 while i < cookies_p.ct    and
					                cookies[i] ~= 0     and
					                cookies[i] ~= @'\r' and
									cookies[i] ~= @'\n' do -- cover all the bases
						if val.ptr == nil then
							if cookies[i] == @'=' then
								key.ct = (cookies + i) - key.ptr
								val.ptr = cookies + i + 1
							end
							i = i + 1
						else
							if cookies[i] == @';' then
								val.ct = (cookies + i) - val.ptr
								if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename],key.ct)) == 0 then
									goto foundcookie
								end

								i = i + 1
								i = lib.str.ffw(cookies + i, cookies_p.ct - i) - cookies
								key.ptr = cookies + i
								val.ptr = nil
							else i = i + 1 end
						end
					end
					if val.ptr == nil then goto nocookie end
					val.ct = (cookies + i) - val.ptr
					if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then
						goto nocookie
					end
					::foundcookie:: do
						var aid, tp = lib.session.cookie_interpret(server.cfg.secret,
							[lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct},
							lib.osclock.time(nil))
						if aid ~= 0 then co.aid = aid co.aid_issue = tp end
					end ::nocookie::;
				end

				if co.aid ~= 0 then
					var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue)
					if sess.ok == false then co.aid = 0 co.aid_issue = 0 else
						co.who = usr.ptr
						var pows = server:actor_powers_fetch(co.who.id)
						var privs = sess.val.privs
						if not privs.post()     then (pows.post     << false) end
						if not privs.edit()     then (pows.edit     << false) end
						if not privs.account()  then (pows.account  << false) end
						if not privs.artifact() then (pows.artifact << false) end
						if not privs.invite()   then (pows.invite   << false) end
						if not privs.moderate() then
							(pows.censor     << false)
							(pows.discipline << false)
							(pows.vacate     << false)
							(pows.crier      << false)
						end
						if not privs.admin() then
							(pows.cred    << false)
							(pows.elevate << false)
							(pows.demote  << false)
							(pows.rebrand << false)
							(pows.herald  << false)
							(pows.config  << false)
							(pows.purge   << false)
						end
						co.who.rights.powers = pows
						var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent')
						if hueok then co.ui_hue = userhue end
					end
				end

				var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival')
				if livelast_p ~= nil and livelast_p.ptr ~= nil then
					var ll, ok = lib.math.decparse(pstring{ptr = livelast_p.ptr, ct = livelast_p.ct - 1})
					if ok then co.live_last = ll end
				end


				var uridec = server.pool:alloc(int8, msg.uri.len)
				var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1)

				var uri = uridec
				if urideclen == -1 then
					for i = 0,msg.uri.len do
						if msg.uri.ptr[i] == @'+'
							then uri.ptr[i] = @' '
							else uri.ptr[i] = msg.uri.ptr[i]
						end
					end
					uri.ct = msg.uri.len
				else uri.ct = urideclen end
				lib.dbg('routing URI ', {uri.ptr, uri.ct})
				
				if lib.str.ncmp('GET', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.get]
				elseif lib.str.ncmp('POST', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.post]
				elseif lib.str.ncmp('HEAD', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.head]
				elseif lib.str.ncmp('OPTIONS', msg.method.ptr, msg.method.len) == 0 then
					co.method = [lib.http.method.options]
				else
					co:complain(400,'unknown method','you have submitted an invalid http request')
					goto fail
				end
				-- check for a content-type header, and see if it's a multipart/
				-- form-data encoded POST request so we can handle file uploads
				co.uploads.sz = 0 co.uploads.run = 0
				if co.method == [lib.http.method.post] then
					var ctt = lib.http.findheader(msg, 'Content-Type')
					if ctt ~= nil then
						if lib.str.ncmp(ctt.ptr,'multipart/form-data;',20) == 0 then
							var p = lib.str.ffw(ctt.ptr + 20,ctt.ct-20)
							if lib.str.ncmp(p,'boundary=',9) ~= 0 then
								co:complain(400,'bad request','unrecognized content-type')
								goto fail
							end
							var boundary = pstring {ptr=p+9,ct=ctt.ct - ((p - ctt.ptr) + 9)}
							co.method = lib.http.method.post_file
							co.uploads:init(8)

							var bsr = (lib.str.acc{}):compose('\r\n--',boundary,'\r\n'):finalize()

							var upmap = lib.str.splitmap(co.body,bsr,8)
							-- first entry may not be preceded by header-break
							if lib.str.find(upmap(0), pstring {
								ptr = bsr.ptr + 2, ct = bsr.ct - 2
							}):ref() then
								upmap(0).ptr = upmap(0).ptr + (bsr.ct - 2)
								upmap(0).ct = upmap(0).ct - (bsr.ct - 2)
							end

							-- last entry is weird
							do var lsr = (lib.str.acc{}):compose('\r\n--',boundary,'--\r\n'):finalize()
								var lsent = upmap.ptr + (upmap.ct - 1)
								var halt = lib.str.find(@lsent, lsr)
								if halt:ref() then
									lsent.ct = halt.ptr - lsent.ptr
								end
								lsr:free() end

							for i=0,upmap.ct do
								var hdrbrk = lib.str.find(upmap(i), '\r\n\r\n')
								if hdrbrk:ref() then
									var hdrtxt = pstring {upmap(i).ptr,upmap(i).ct - hdrbrk.ct}
									var hdrs = lib.str.splitmap(hdrtxt, '\r\n',6)
									var ctt = pstring.null()
									var ctd = pstring.null()
									for j=0, hdrs.ct do
										var brk = lib.str.find(hdrs(j),':')
										if brk:ref() then
											var hdr = pstring{hdrs(j).ptr,hdrs(j).ct - brk.ct}
											var val = pstring{brk.ptr+1, brk.ct-1}:ffw()
											if hdr:cmp('Content-Type') then
												ctt = val
											elseif hdr:cmp('Content-Disposition') then
												ctd = val
											end
										end
									end
									if ctd:ref() then
										var ctdvals = lib.str.splitmap(ctd, ';', 4) defer ctdvals:free()
										if ctdvals(0):cmp('form-data') and ctdvals.ct > 1 then
											var fld = pstring.null()
											var file = pstring.null()
											for j=1, ctdvals.ct do var v = ctdvals(j):ffw()
												var x = lib.str.find(v,'=')
												if x:ref() then
													var key = pstring{v.ptr, v.ct - x.ct}
													var val = pstring{x.ptr + 1, x.ct - 1}
													var decval, ofs, sp = lib.str.toknext(val,@';',true)
													if key:cmp('name') then
														fld = decval
													elseif key:cmp('filename') then
														file = decval
													else decval:free() end
												end
											end
											if fld:ref() then
												var nextup = co.uploads:new()
												if ctt:ref() then
													nextup.ctype = ctt
												else
													nextup.ctype = pstring.null()
												end
												nextup.body = pstring {
													ptr = hdrbrk.ptr + 4;
													ct = hdrbrk.ct - 4;
												}
												nextup.ctype = ctt
												nextup.field = fld
												nextup.filename = file
											end
										end
									end
								end
							end
							bsr:free()
							upmap:free()
						end
					end
				end

				route.dispatch_http(&co, uri, co.method)

				::fail::
				if co.uploads.run > 0 then
					for i=0,co.uploads.sz do
						co.uploads(i).filename:free()
						co.uploads(i).field:free()
					end
					co.uploads:free()
				end

				if co.aid ~= 0 then lib.mem.heapf(co.who) end
				-- if co.varbuf.ptr ~= nil then co.varbuf:free() end
				-- if co.navbar.ptr ~= nil then co.navbar:free() end
				co.actorcache:free()
				server.pool:clear()
			end
		end
	end;
}

local terra cfg(s: &srv, befile: rawstring)
	lib.report('configuring backends from ', befile)

	var fr = lib.file.open(befile, [lib.file.mode.read])
	if fr.ok == false then
		lib.bail('could not open configuration file ', befile)
	end

	var f = fr.val
	var c: lib.mem.vec(lib.store.source) c:init(8)
	var text: lib.str.acc text:init(256)
	do var buf: int8[64]
		while true do
			var ct = f:read(buf, [buf.type.N])
			if ct == 0 then break end
			text:push(buf, ct)
		end
	end
	f:close()

	var cur = text.buf
	var segs: tuple(&int8, &int8)[3] = array(
		{[&int8](0),[&int8](0)},
		{[&int8](0),[&int8](0)},
		{[&int8](0),[&int8](0)}
	)
	var segdup = [terra(s: {rawstring, rawstring})
		var sz = s._1 - s._0
		var str = s._0
		return [lib.mem.ptr(int8)] {
			ptr = lib.str.ndup(str, sz);
			ct = sz;
		}
	end]
	var fld = 0
	while (cur - text.buf) < text.sz do
		if segs[fld]._0 == nil then
			if not (@cur == @' ' or @cur == @'\t' or @cur == @'\n') then
				segs[fld] = {cur, nil}
			end
		else
			if fld < 2 and @cur == @' ' or @cur == @'\t' then
				segs[fld]._1 = cur
				fld = fld + 1
				segs[fld] = {nil, nil}
			elseif @cur == @'\n' or cur == text.buf + (text.sz-1) then
				if fld < 2 then lib.bail('incomplete backend line in ', befile) else
					segs[fld]._1 = cur
					var src = c:new()
					src.id = segdup(segs[0])
					src.string = segdup(segs[2])
					src.backend = nil
					for i = 0,[lib.store.backends.type.N] do
						if lib.str.ncmp(segs[1]._0, lib.store.backends[i].id, segs[1]._1 - segs[1]._0) == 0 then
							src.backend = &lib.store.backends[i]
							break
						end
					end
					if src.backend == nil then
						lib.bail('unknown backend in ', befile)
					end
					src.handle = nil
					fld = 0
					segs[0] = {nil, nil}
				end
			end
		end
		cur = cur + 1
	end
	text:free()

	if c.sz > 0 then
		s.sources = c:crush()
	else
		s.sources.ptr = nil
		s.sources.ct = 0
	end
end

terra srv:actor_stats(uid: uint64)
	var stats = lib.store.actor_stats {
		posts = 0, mutuals = 0;
		follows = 0, followers = 0;
	}
	for i=0,self.sources.ct do
		var s = self.sources.ptr[i]:actor_stats(uid)
		stats.posts     = stats.posts     + s.posts
		stats.mutuals   = stats.mutuals   + s.mutuals
		stats.followers = stats.followers + s.followers
		stats.follows   = stats.follows   + s.follows
	end
	return stats
end

terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring)
	var cs: lib.store.credset cs:clear()
	var ok = false
	for i=0,self.sources.ct do
		var set, iok = self.sources(i):actor_auth_how(ip, usn)
		if iok then
			cs = cs + set
			ok = iok
		end
	end
	return cs, ok
end

local function mk_auth_fn(suffix,...)
	local syms = {...}
	local name = 'actor_auth_' .. suffix
	srv.methods[name] = terra(self: &srv, ip: lib.store.inet, user: pstring, [syms]): uint64
		for i=0,self.sources.ct do
			if self.sources(i).backend ~= nil and
			   self.sources(i).backend.[name] ~= nil then
				var aid,uid,newhnd = self.sources(i):[name](ip,user, [syms])
				if aid ~= 0 then
					if uid == 0 then
						lib.dbg('new user just logged in, creating account entry')
						var kbuf: uint8[lib.crypt.const.maxdersz]
						var na = lib.store.actor.mk(&kbuf[0])
						na.handle = newhnd.ptr
						var newuid: uint64
						if self.sources(i).backend.actor_create ~= nil then
							newuid = self.sources(i):actor_create(&na)
						else newuid = self:actor_create(&na) end

						if self.sources(i).backend.actor_auth_register_uid ~= nil then
							self.sources(i):actor_auth_register_uid(aid,newuid)
						end
					end
					return aid
				end
			end
		end

		return 0
	end
	srv.methods[name].name = name
end

mk_auth_fn('pw',        symbol(pstring))
mk_auth_fn('challenge', symbol(lib.mem.ptr(uint8)), symbol(pstring))

--terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
--	for i=0,self.sources.ct do
--		if self.sources(i).backend ~= nil and
--		   self.sources(i).backend.actor_auth_pw ~= nil then
--			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
--			if aid ~= 0 then
--				if uid == 0 then
--					lib.dbg('new user just logged in, creating account entry')
--					var kbuf: uint8[lib.crypt.const.maxdersz]
--					var na = lib.store.actor.mk(&kbuf[0])
--					na.handle = newhnd.ptr
--					var newuid: uint64
--					if self.sources(i).backend.actor_create ~= nil then
--						newuid = self.sources(i):actor_create(&na)
--					else newuid = self:actor_create(&na) end
--
--					if self.sources(i).backend.actor_auth_register_uid ~= nil then
--						self.sources(i):actor_auth_register_uid(aid,newuid)
--					end
--				end
--				return aid
--			end
--		end
--	end
--
--	return 0
--end

terra cfgcache.methods.load :: {&cfgcache} -> {}
terra cfgcache:init(o: &srv)
	self.overlord = o
	self:load()
end

terra srv:setup(befile: rawstring)
	cfg(self, befile)
	var success = false
	if self.sources.ct == 0 then lib.bail('no data sources specified') end
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('opening data source ', src.id.ptr, '(', src.backend.id, ')')
		src.handle = src.backend.open(src)
		if src.handle ~= nil then success = true end
	end
	if not success then
		lib.bail('could not connect to any data sources!')
	end
end

terra srv:start(iname: rawstring)
	self:conprep(lib.store.prepmode.full)
	self.cfg:init(self)
	self.pool:init(self.cfg.poolinitsz)
	var dbbind = self:conf_get('bind')
	if iname == nil then iname = lib.proc.getenv('parsav_instance') end
	if iname == nil then
		self.id = self.cfg.instance.ptr;
		-- let this leak -- it'll be needed for the lifetime of the process anyway
	else self.id = iname end 

	if iname ~= nil then
		lib.report('parsav instance "',iname,'" starting')
	end

	var envbind = lib.proc.getenv('parsav_bind')
	var bind: rawstring
	if envbind ~= nil then
		bind = envbind
	elseif dbbind.ptr ~= nil then
		bind = dbbind.ptr
	else bind = '[::1]:10917' end

	lib.report('binding to ', bind)
	lib.net.mg_mgr_init(&self.webmgr)
	self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)

	if dbbind.ptr ~= nil then dbbind:free() end
end

terra srv:poll()
	lib.net.mg_mgr_poll(&self.webmgr,300)
end

terra srv:shutdown()
	lib.net.mg_mgr_free(&self.webmgr)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
		src:close()
	end
	self.sources:free()
	self.pool:free()
end

terra cfgcache:cfint(name: rawstring, default: intptr)
	var str = self.overlord:conf_get(name)
	if str.ptr ~= nil then
		var i,ok = lib.math.decparse(str)
		if ok then default = i else
			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead')
		end
		str:free()
	end
	return default
end

terra cfgcache:cffsz(name: rawstring, default: intptr)
	var str = self.overlord:conf_get(name)
	if str:ref() then
		var sz, ok = lib.math.fsz_parse(str)
		if ok then default = sz else
			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected byte length; using default value instead')
		end
		str:free()
	end
	return default
end

terra cfgcache:cfbool(name: rawstring, default: bool)
	var str = self.overlord:conf_get(name)
	if str.ptr ~= nil then
		if str:cmp('true') or str:cmp('on') or
		   str:cmp('yes')  or str:cmp('1') then
			default = true
		elseif str:cmp('false') or str:cmp('off') or
		       str:cmp('no')    or str:cmp('0') then
			default = false
		else
			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead')
		end
		str:free()
	end
	return default
end

terra cfgcache:load()
	self.instance = self.overlord:conf_get('instance-name')
	self.secret = self.overlord:conf_get('server-secret')

	self.pol_reg = self:cfbool('policy-self-register', false)
	self.pol_autoherald = self:cfbool('policy-self-herald', true)

	do self.credmgd = false
	var sreg = self.overlord:conf_get('credential-store')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'managed') == 0
			then self.credmgd = true
			else self.credmgd = false
		end
		sreg:free()
	end end

	self.maxupsz = self:cffsz('maximum-artifact-size', [1024 * 100]) -- 100 kilobyte default
	self.poolinitsz = self:cffsz('server-pool-size-initial', [1024 * 10]) -- 10 kilobyte default
	
	self.pol_sec = secmode.lockdown
	var smode = self.overlord:conf_get('policy-security')
	if smode.ptr ~= nil then
		if lib.str.cmp(smode.ptr, 'public') == 0 then
			self.pol_sec = secmode.public
		elseif lib.str.cmp(smode.ptr, 'private') == 0 then
			self.pol_sec = secmode.private
		elseif lib.str.cmp(smode.ptr, 'lockdown') == 0 then
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end
		smode:free()
	end

	self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
	self.nranks = self:cfint('user-ranks',10)
	self.maxinvites = self:cfint('max-invites',64)
	
	var webmaster = self.overlord:conf_get('master')
	if webmaster:ref() then defer webmaster:free()
		var wma = self.overlord:actor_fetch_xid(webmaster)
		if not wma then
			lib.warn('the webmaster specified in the configuration store does not seem to exist or is not known to this instance; preceding as if no master defined. if the master is a remote user, you can rectify this with the `actor "',{webmaster.ptr,webmaster.ct},'" instantiate` and `conf refresh` commands')
		else
			self.master = wma(0).id
			wma:free()
		end
	end

	self.ui_cue_staff = self.overlord:conf_get('ui-profile-cue-staff')
	self.ui_cue_founder = self.overlord:conf_get('ui-profile-cue-master')

	self.usrdef_pol_follow = self.overlord:conf_get('user-default-acl-follow')
	self.usrdef_pol_follow_req = self.overlord:conf_get('user-default-acl-follow-req')
end

return {
	overlord = srv;
	convo = convo;
	route = route;
	secmode = secmode;
}
