-- vim: ft=terra
local srv = ...
local pstring = lib.str.t

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: pstring
-- cache
	ui_hue: uint16
	navbar: pstring
	actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries
-- private
	varbuf: pstring
	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:matchmime(mime: lib.http.mime.t): bool
	return self.reqtype == [lib.http.mime.none]
		or self.reqtype == mime
end

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

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_trusted(lockdown: bool, mime: pstring, data: lib.mem.ptr(uint8))
	var lockhdr = "Content-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\n"
	if not lockdown then lockhdr = "" end
	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr)
	lib.net.mg_send(self.con, data.ptr, data.ct)
	lib.net.mg_send(self.con, '\r\n', 2)
end

terra convo:json(data: pstring)
	self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
end

terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
	var ty = lib.mime.lookup(mime)
	if ty == nil then
		lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not in database!')
		mime = 'application/x-octet-stream'
	else
		if not ty.safe then
			lib.dbg("mime type ", {mime.ptr,mime.ct}, ' not safe!')
			if ty.binary then
				mime = 'application/x-octet-stream'
			else
				mime = 'text/plain'
			end
		end
	end
	self:bytestream_trusted(true, mime, data)
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: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)

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

	if self:matchmime(lib.http.mime.html) then
		var body = [convo.page] {
			title = self:qstr('error :: ', title);
			body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>');
			class = 'error';
			cache = false;
		}

		self:statpage(code, body)
	else
		var pg = lib.http.page { respcode = code, body = pstring.null() }
		var ctt = lib.http.mime.none
		if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
			pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
				{ekind = title, edesc = msg}):poolstr(&self.srv.pool)
		elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
			pg.body = self:qstr('error: ',title,'\n',msg)
		elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
			pg.body = self:qstr('# error :: ',title,'\n\n',msg)
		elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
			pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
		end
		var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
		if ctt == lib.http.mime.none then
			pg.headers.ct = 0
		else
			pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
			switch ctt do
				escape
					for key,ty in ipairs(lib.mime.types) do
						if key ~= 'none' and lib.http.mime[key] ~= nil then
							emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [ty.id[1]] end end
						end
					end
				end
			end
		end
		pg:send(self.con)
	end
end

terra convo:fail(code: uint16)
	switch code do
		escape
			local stderrors = {
				{400, 'bad request', "the action you have attempted on this resource is not meaningful"};
				{401, 'unauthorized', "this resource is not available at your clearance level"};
				{403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
				{404, 'resource not found', "that resource is not extant on or known to this server"};
				{405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
				{406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
				{500, 'internal server error', "parsav did a fucksy wucksy"};
			}

			for i,v in ipairs(stderrors) do
				emit quote case uint16([v[1]]) then
					self:complain([v])
				end end
			end
		end
		else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
	end
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

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

return convo
