-- vim: ft=terra
local m = {
	timepoint = lib.osclock.time_t;
	scope = lib.enum {
		'public', 'private', 'local';
		'personal', 'direct', 'circle';
	};
	noticetype = lib.enum {
		'none', 'mention', 'reply', 'like', 'rt', 'react', 'follow'
	};

	relation = lib.set {
		'follow',
		'subscribe', -- get a notification for every post
		'mute', -- posts will be completely hidden at all times
		'block', -- no interactions will be permitted, but posts will remain visible
		'silence', -- messages will not be accepted
		'collapse', -- posts will be collapsed by default
		'disemvowel', -- posts will be ritually humiliated, but shown
		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
		'exclude', -- own posts will not be visible to this user
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'account', 'upload', 'artifact', 'moderate', 'admin', 'invite'
	};
	powerset = lib.set {
		-- user powers -- default on
		'login', -- not locked out
		'visible', -- account & posts can be seen by others
		'post', -- can do poasts
		'shout', -- posts show up on local timeline
		'propagate', -- posts are sent to other instances
		'artifact', -- upload, claim, and manage artifacts
		'account', -- configure own account
		'edit'; -- edit own poasts
		'snitch'; -- can issue badthink reports

		-- admin powers -- default off
		'purge', -- permanently delete users
		'config', -- change daemon policy & config UI
		'censor', -- dispose of badthink
		'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
		'vacate', -- can remove others' injunctions, but not apply them
		'cred', -- alter credentials
		'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
		'rebrand', -- modify site's brand identity
		'herald', -- grant serverwide epithets and badges
		'crier', -- can promote content to the instance page
		'invite' -- *unlimited* invites
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

local function setmap(set)
	local map = {}
	local struct pt { name:lib.mem.ptr(int8), val:set }
	for k,v in pairs(set.members) do
		map[#map + 1] = quote
			var ps: set ps:clear()
			(ps.[v] << true)
		in pt {name = [v], val = ps} end
	end
	return map
end
m.powmap = setmap(m.powerset)
m.privmap = setmap(m.privset)

terra m.powerset:affect_users()
	return self.purge() or self.discipline() or self.herald() or
	       self.elevate() or self.demote() or self.cred()
end

local str = rawstring
local pstr = lib.mem.ptr(int8)

struct m.source

struct m.rights {
	rank: uint16 -- lower = more powerful except 0 = regular user
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	invites: uint32 -- # of people left this user can invite
	
	powers: m.powerset
}

terra m.rights_default() -- TODO make configurable
	var pow: m.powerset pow:clear()
	(pow.login     << true)
	(pow.visible   << true)
	(pow.post      << true)
	(pow.shout     << true)
	(pow.propagate << true)
	(pow.artifact  << true)
	(pow.account   << true)
	(pow.edit      << true)
	(pow.snitch    << true)
	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
	bio: str
	epithet: str
	avatar: str
	avatarid: uint64
	knownsince: m.timepoint
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
	source: &m.source
}

terra m.actor:outranks(other: &m.actor)
 -- this predicate determines where two users stand relative to
 -- each other in the formal staff hierarchy. it is used in
 -- authority calculations, but this function should only be
 -- used directly in rendering code and by other predicates.
 -- do not use it in authority calculation, as there are special
 -- cases where formal rank does not fully determine a user's
 -- capabilities (e.g. roots have the same rank, but can
 -- exercise power over each other, unlike lower ranks)
	if self.rights.rank == 0 then
	 -- peons never outrank anybody
		return false
	end
	if other.rights.rank == 0 then
	 -- everybody outranks peons
		return true
	end
	return self.rights.rank < other.rights.rank
	-- rank 1 is the highest possible, rank 2 is second-highest, and so on
end

terra m.actor:overpowers(other: &m.actor)
 -- this predicate determines whether one user may exercise their
 -- powers over another user. it does not affect what those powers
 -- actually are (for instance, you cannot revoke a power you do
 -- not have, no matter how much you outrank someone)
	if self.rights.rank == 1 and other.rights.rank == 1 then
	 -- special case: root users always overpower each other
	 -- otherwise, nobody could reset their passwords
	 -- (also dissuades people from giving root lightly)
		return true
	end
	return self:outranks(other)
end

terra m.actor.methods.handle_validate(hnd: rawstring)
	if hnd[0] == 0 then
		return false
	end
	-- TODO validate fully
	return true
end

terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();
		avatarid = 0;
		epithet = nil, key = [lib.mem.ptr(uint8)] {
			ptr = &kbuf[0], ct = privsz
		};
	}
end

struct m.actor_stats {
	posts: intptr
	follows: intptr
	followers: intptr
	mutuals: intptr
}

struct m.range {
	mode: uint8 -- 0 == I->I, 1 == T->I, 2 == I->T, 3 == T->T
	union {
		from_time: m.timepoint
		from_idx: uint64
	}
	union {
		to_time: m.timepoint
		to_idx: uint64
	}
}

terra m.range:matrix()
	if self.mode == 0 then
		return self.from_time,self.to_time,0,0
	elseif self.mode == 1 then
		return self.from_time,0,self.to_idx,0
	elseif self.mode == 2 then
		return 0,self.to_time,0,self.from_idx
	elseif self.mode == 3 then
		return 0,0,self.to_idx,self.from_idx
	else lib.bail('invalid mode on timeline range!') end
end

struct m.post {
	id: uint64
	author: uint64
	subject: str
	body: str
	acl: str
	posted: m.timepoint
	discovered: m.timepoint
	edited: m.timepoint
	chgcount: uint
	mentions: lib.mem.ptr(uint64)
	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
	convoheaduri: str
	parent: uint64
-- ephemera
	localpost: bool
	accent: int16
	rts: uint32
	likes: uint32
	rtdby: uint64 -- 0 if not rt
	rtact: uint64 -- 0 if not rt, id of rt action otherwise
	isreply: bool
	source: &m.source

	-- save :: bool -> {} (defined in acl.t due to dep. hell)
}

struct m.artifact {
	rid: uint64
	owner: uint64
	desc: str
	folder: str
	mime: str
	url: str
}

m.user_conf_funcs = function(be,n,ty,rty,rty2)
	rty = rty or ty
	local gt
	if not rty2 -- what the fuck?
		then gt = {&m.source, uint64, rawstring} -> rty;
		else gt = {&m.source, uint64, rawstring} -> {rty, rty2};
	end
	for k, t in pairs {
		enum = {&m.source, uint64, rawstring} -> lib.mem.ptr(rty);
		get = gt;
		set = {&m.source, uint64, rawstring, ty} -> {};
		reset = {&m.source, uint64, rawstring} -> {};
	} do
		be.entries[#be.entries+1] = {
			field = 'actor_conf_'..n..'_'..k, type = t
		}
	end
end

struct m.notice {
	kind: m.noticetype.t
	when: uint64
	who: uint64
	what: uint64
	union {
		reply: uint64
		reaction: int8[32] -- are you shitting me, unichode
	}
}

struct m.inet {
	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
	union {
		v4: uint8[4]
		v6: uint8[16]
	}
	union {
		fixbits: uint8 -- for cidr
		port: uint16 -- for origin
	}
}

terra m.inet:cidr_str()
	if self.pv == 4 then
		var maxsz = 3*4 + 3 + 1
	elseif self.pv == 6 then
		var bits = 128
		var bytes = bits / 8
		var hexchs = bytes * 2
		var segs = hexchs / 4
		var seps = segs - 1
		var maxsz = hexchs + seps + 1
	else return nil end
end

struct m.kompromat {
-- The Evidence
	id: uint64
	perp: uint64 -- whodunnit
	desc: str
	post: uint64 -- the post in question, if any
	reporter: uint64 -- 0 = originated automatically by the System itself
	resolution: str -- null for unresolved
	-- as proto: set resolution to empty string to search for resolved incidents
}

struct m.sanction {
	id: uint64
	issuer: uint64
	scope: uint64
	nature: uint16
	victim: uint64
	autoexpire: bool  expire: m.timepoint
	timedreview: bool review: m.timepoint
	reason: str
	context: str
}

struct m.auth {
-- a credential record
	aid: uint64
	uid: uint64
	kind: str
	aname: str
	comment: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}

struct m.relationship {
	agent: uint64
	patient: uint64
	rel: m.relation -- agent → patient
	recip: m.relation -- patient → agent
}

-- backends only handle content on the local server
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database

	tx_enter: &m.source -> bool
	tx_complete: &m.source -> bool
	-- these two functions are special, in that they should be called
	-- directly on a specific backend, rather than passed down to the
	-- backends by the server; that is pathological behavior that will
	-- not have the desired effect

	server_setup_self: {&m.source, rawstring, lib.mem.ptr(uint8)} -> {}

	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
	conf_set: {&m.source, rawstring, rawstring} -> {}
	conf_reset: {&m.source, rawstring} -> {}

	actor_create: {&m.source, &m.actor} -> uint64
	actor_save: {&m.source, &m.actor} -> {}
	actor_save_privs: {&m.source, &m.actor} -> {}
	actor_purge_uid: {&m.source, uint64} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_enum: {&m.source} -> lib.mem.lstptr(m.actor)
	actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats
	actor_rel: {&m.source, uint64, uint64} -> m.relationship

	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
			-- username: rawstring
	actor_auth_otp: {&m.source, m.inet, rawstring, rawstring}
			-> {uint64, uint64, pstr}
	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
			-> {uint64, uint64, pstr}
		-- handles password-based logins against hashed passwords
			-- origin: inet
			-- handle: rawstring
			-- token:  rawstring
	actor_auth_tls:    {&m.source, m.inet, rawstring}
			-> {uint64, uint64, pstr}
		-- handles implicit authentication performed as part of an TLS connection
			-- origin: inet
			-- fingerprint: rawstring
	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
			-> {uint64, uint64, pstr}
		-- handles API authentication
			-- origin: inet
			-- handle: rawstring
			-- key:    rawstring (X-API-Key)
	actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
	actor_powers_fetch: {&m.source, uint64} -> m.powerset
	actor_session_fetch: {&m.source, uint64, m.inet, m.timepoint} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
		-- retrieves an auth record + actor combo suitable by AID suitable
		-- for determining session validity & caps
			-- aid:    uint64
			-- origin: inet
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64
	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> uint64
	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_privs_set: {&m.source, uint64, m.privset} -> {}
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
	auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
		-- authentication tokens and accounts have a property that controls
		-- whether auth cookies dated to a certain point are valid. cookies
		-- that are generated before the timepoint are considered invalid.
		-- this is used primarily to lock out untrusted sessions.
			-- uid: uint64
	auth_sigtime_user_alter: {&m.source, uint64, m.timepoint} -> {}
			-- uid: uint64
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_destroy: {&m.source, uint64} -> {}
	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool
	post_retweet: {&m.source, uint64, uint64, bool} -> {}
	post_like: {&m.source, uint64, uint64, bool} -> {}
			-- undo: bool
	post_react: {&m.source, uint64, uint64, pstring} -> {}
			-- emoji: pstring (null to delete previous reaction, otherwise adds/changes)
	post_act_cancel: {&m.source, uint64} -> {}
	post_liked_uid: {&m.source, uint64, uint64} -> bool
	post_reacted_uid: {&m.source, uint64, uint64} -> bool
	post_act_fetch_notice: {&m.source, uint64} -> m.notice

	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint

	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea
			-- mime:     pstring
	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
		-- checks whether a hash is already in the database without uploading
		-- the entire file to the database server
			-- hash: bytea
				--> artifact id (0 if null), suppressed?
	artifact_expropriate: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
		-- claims an existing artifact for the user's own collection
			-- uid:         uint64
			-- artifact id: uint64
			-- description: pstring
			-- folder:      pstring
	artifact_claim_alter: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
		-- edits an existing claim to an artifact
			-- ibid
	artifact_disclaim: {&m.source, uint64, uint64} -> {}
		-- a user disclaims their ownership stake in an artifact, removing it from
		-- the database entirely if they were the only owner, and removing their
		-- description of it either way
			-- uid:         uint64
			-- artifact id: uint64
	artifact_excise: {&m.source, uint64, bool} -> {}
		-- (admin action) forcibly excise an artifact from the database, deleting
		-- all links to it and removing it from users' collections. if "blacklist,"
		-- the artifact will be banned and attempts to upload it in the future
		-- will fail, triggering a report. mainly intended for dealing with spam,
		-- IP violations, That Which Shall Not Be Named, and various other infohazards.
			-- artifact id: uint64
			-- blacklist:   bool
	artifact_enum_uid: {&m.source, uint64, lib.str.t} -> lib.mem.lstptr(m.artifact)
		-- produces a list of artifacts claimed by a user, optionally
		-- restricted by folder (empty string = new only)
	artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact)
		-- fetch a user's view of an artifact
			-- uid: uint64
			-- rid: uint64
	artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
		-- load the body of an artifact into memory (also returns mime)
	artifact_folder_enum: {&m.source, uint64} -> lib.mem.ptr(lib.str.t)
		-- enumerate all of a user's folders

	nkvd_report_issue: {&m.source, &m.kompromat} -> {}
		-- an incidence of Badthink has been detected. report it immediately
		-- to the Supreme Soviet
	nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
		-- search through the Archives
			-- proto: kompromat (null for all records, or a prototype describing the records to return)
	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
	nkvd_sanction_vacate: {&m.source, uint64} -> {}
	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
	nkvd_sanction_review: {&m.source, m.timepoint} -> {}

	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post)
}

m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)

struct m.source {
	backend: &m.backend
	id: lib.mem.ptr(int8)
	handle: &opaque
	string: lib.mem.ptr(int8)
}
terra m.source:free()
	self.id:free()
	self.string:free()
end
m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
	local q = {...}
	-- syntax sugar to forward unrecognized calls onto the backend
	return quote var r = obj.backend.[meth](&obj, [q]) in r end
end)

return m
