let g_about_cache = {};

async function query(sql, args) {
	let result = [];
	await ssb.sqlAsync(sql, args, function (row) {
		result.push(row);
	});
	return result;
}

async function contacts_internal(id, last_row_id, following, max_row_id) {
	let result = Object.assign({}, following[id] || {});
	result.following = result.following || {};
	result.blocking = result.blocking || {};
	let contacts = await query(
		`
				SELECT content FROM messages
				WHERE author = ? AND
				rowid > ? AND
				rowid <= ? AND
				json_extract(content, '$.type') = 'contact'
				ORDER BY sequence
			`,
		[id, last_row_id, max_row_id]
	);
	for (let row of contacts) {
		let contact = JSON.parse(row.content);
		if (contact.following === true) {
			result.following[contact.contact] = true;
		} else if (contact.following === false) {
			delete result.following[contact.contact];
		} else if (contact.blocking === true) {
			result.blocking[contact.contact] = true;
		} else if (contact.blocking === false) {
			delete result.blocking[contact.contact];
		}
	}
	following[id] = result;
	return result;
}

async function contact(id, last_row_id, following, max_row_id) {
	return await contacts_internal(id, last_row_id, following, max_row_id);
}

async function following_deep_internal(
	ids,
	depth,
	blocking,
	last_row_id,
	following,
	max_row_id
) {
	let contacts = await Promise.all(
		[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
	);
	let result = {};
	for (let i = 0; i < ids.length; i++) {
		let id = ids[i];
		let contact = contacts[i];
		let all_blocking = Object.assign({}, contact.blocking, blocking);
		let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
		let deeper =
			depth > 1
				? await following_deep_internal(
						found,
						depth - 1,
						all_blocking,
						last_row_id,
						following,
						max_row_id
					)
				: [];
		result[id] = [id, ...found, ...deeper];
	}
	return [...new Set(Object.values(result).flat())];
}

async function following_deep(ids, depth, blocking) {
	let db = await database('cache');
	const k_cache_version = 5;
	let cache = await db.get('following');
	cache = cache ? JSON.parse(cache) : {};
	if (cache.version !== k_cache_version) {
		cache = {
			version: k_cache_version,
			following: {},
			last_row_id: 0,
		};
	}
	let max_row_id = (
		await query(
			`
			SELECT MAX(rowid) AS max_row_id FROM messages
		`,
			[]
		)
	)[0].max_row_id;
	let result = await following_deep_internal(
		ids,
		depth,
		blocking,
		cache.last_row_id,
		cache.following,
		max_row_id
	);
	cache.last_row_id = max_row_id;
	let store = JSON.stringify(cache);
	await db.set('following', store);
	return result;
}

async function fetch_about(db, ids, users) {
	const k_cache_version = 1;
	let cache = await db.get('about');
	cache = cache ? JSON.parse(cache) : {};
	if (cache.version !== k_cache_version) {
		cache = {
			version: k_cache_version,
			about: {},
			last_row_id: 0,
		};
	}
	let max_row_id = 0;
	await ssb.sqlAsync(
		`
			SELECT MAX(rowid) AS max_row_id FROM messages
		`,
		[],
		function (row) {
			max_row_id = row.max_row_id;
		}
	);
	for (let id of Object.keys(cache.about)) {
		if (ids.indexOf(id) == -1) {
			delete cache.about[id];
		}
	}

	let abouts = [];
	await ssb.sqlAsync(
		`
				SELECT
					messages.*
				FROM
					messages,
					json_each(?1) AS following
				WHERE
					messages.author = following.value AND
					messages.rowid > ?3 AND
					messages.rowid <= ?4 AND
					json_extract(messages.content, '$.type') = 'about'
				UNION
				SELECT
					messages.*
				FROM
					messages,
					json_each(?2) AS following
				WHERE
					messages.author = following.value AND
					messages.rowid <= ?4 AND
					json_extract(messages.content, '$.type') = 'about'
				ORDER BY messages.author, messages.sequence
			`,
		[
			JSON.stringify(ids.filter((id) => cache.about[id])),
			JSON.stringify(ids.filter((id) => !cache.about[id])),
			cache.last_row_id,
			max_row_id,
		]
	);
	for (let about of abouts) {
		let content = JSON.parse(about.content);
		if (content.about === about.author) {
			delete content.type;
			delete content.about;
			cache.about[about.author] = Object.assign(
				cache.about[about.author] || {},
				content
			);
		}
	}
	cache.last_row_id = max_row_id;
	await db.set('about', JSON.stringify(cache));
	users = users || {};
	for (let id of Object.keys(cache.about)) {
		users[id] = Object.assign(users[id] || {}, cache.about[id]);
	}
	return Object.assign({}, users);
}

async function getAbout(db, id) {
	if (g_about_cache[id]) {
		return g_about_cache[id];
	}
	let o = await db.get(id + ':about');
	const k_version = 4;
	let f = o ? JSON.parse(o) : o;
	if (!f || f.version != k_version) {
		f = {about: {}, sequence: 0, version: k_version};
	}
	await ssb.sqlAsync(
		'SELECT ' +
			'  sequence, ' +
			'  content ' +
			'FROM messages ' +
			'WHERE ' +
			'  author = ?1 AND ' +
			'  sequence > ?2 AND ' +
			"  json_extract(content, '$.type') = 'about' AND " +
			"  json_extract(content, '$.about') = ?1 " +
			'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
			'ORDER BY sequence',
		[id, f.sequence],
		function (row) {
			f.sequence = row.sequence;
			if (row.content) {
				let about = {};
				try {
					about = JSON.parse(row.content);
				} catch {}
				delete about.about;
				delete about.type;
				f.about = Object.assign(f.about, about);
			}
		}
	);
	let j = JSON.stringify(f);
	if (o != j) {
		await db.set(id + ':about', j);
	}
	g_about_cache[id] = f.about;
	return f.about;
}

async function getSize(db, id) {
	let size = 0;
	await ssb.sqlAsync(
		'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
		[id],
		function (row) {
			size += row.size;
		}
	);
	return size;
}

async function getSizes(ids) {
	let sizes = {};
	await ssb.sqlAsync(
		`
			SELECT
				author,
				(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
			FROM messages
			JOIN json_each(?) AS ids ON author = ids.value
			GROUP BY author
		`,
		[JSON.stringify(ids)],
		function (row) {
			sizes[row.author] = row.size;
		}
	);
	return sizes;
}

function niceSize(bytes) {
	let value = bytes;
	let unit = 'B';
	const k_units = ['kB', 'MB', 'GB', 'TB'];
	for (let u of k_units) {
		if (value >= 1024) {
			value /= 1024;
			unit = u;
		} else {
			break;
		}
	}
	return Math.round(value * 10) / 10 + ' ' + unit;
}

function escape(value) {
	return value
		.replaceAll('&', '&amp;')
		.replaceAll('<', '&lt;')
		.replaceAll('>', '&gt;');
}

async function main() {
	await app.setDocument('<pre style="color: #fff">building...</pre>');
	let db = await database('ssb');
	let whoami = await ssb.getIdentities();
	let tree = '';
	await app.setDocument(
		`<pre style="color: #fff">Enumerating followed users...</pre>`
	);
	let following = await following_deep(whoami, 2, {});
	await app.setDocument(
		`<pre style="color: #fff">Getting names and sizes...</pre>`
	);
	let [about, sizes] = await Promise.all([
		fetch_about(db, following, {}),
		getSizes(following),
	]);
	await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
	following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
	for (let id of following) {
		tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
	}
	await app.setDocument(
		'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
			tree +
			'</ul>\n</body>\n</html>'
	);
}

main();
