#include "ssb.db.h"

#include "log.h"
#include "mem.h"
#include "ssb.h"
#include "trace.h"
#include "util.js.h"

#include "ow-crypt.h"
#include "sodium/crypto_hash_sha256.h"
#include "sodium/crypto_sign.h"
#include "sodium/crypto_sign_ed25519.h"
#include "sodium/randombytes.h"
#include "sqlite3.h"
#include "uv.h"

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

typedef struct _message_store_t message_store_t;

static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void* user_data);

static int _tf_ssb_db_try_exec(sqlite3* db, const char* statement)
{
	char* error = NULL;
	int result = sqlite3_exec(db, statement, NULL, NULL, &error);
	if (result != SQLITE_OK)
	{
		tf_printf("Error running '%s': %s.\n", statement, error ? error : sqlite3_errmsg(db));
	}
	if (error)
	{
		sqlite3_free(error);
	}
	return result;
}

static void _tf_ssb_db_exec(sqlite3* db, const char* statement)
{
	char* error = NULL;
	int result = sqlite3_exec(db, statement, NULL, NULL, &error);
	if (result != SQLITE_OK)
	{
		tf_printf("Error running '%s': %s.\n", statement, error ? error : sqlite3_errmsg(db));
	}
	if (error)
	{
		sqlite3_free(error);
	}
	if (result != SQLITE_OK)
	{
		abort();
	}
}

static bool _tf_ssb_db_has_rows(sqlite3* db, const char* query)
{
	bool found = false;
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		int result = SQLITE_OK;
		while ((result = sqlite3_step(statement)) == SQLITE_ROW)
		{
			found = true;
		}
		if (result != SQLITE_DONE)
		{
			tf_printf("%s\n", sqlite3_errmsg(db));
			abort();
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("%s\n", sqlite3_errmsg(db));
		abort();
	}
	return found;
}

static int _tf_ssb_db_busy_handler(void* user_data, int count)
{
	return 1;
}

static int _tf_ssb_db_wal_hook(void* user_data, sqlite3* db, const char* db_name, int log_pages)
{
	/* Keeps the log below about 64MB with default 4096 byte pages. */
	if (log_pages >= 16384)
	{
		int log = 0;
		int checkpointed = 0;
		uint64_t checkpoint_start_ns = uv_hrtime();
		if (sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, &log, &checkpointed) == SQLITE_OK)
		{
			tf_printf("Checkpointed %d pages in %d ms.  Log is now %d frames.\n", log_pages, (int)((uv_hrtime() - checkpoint_start_ns) / 1000000LL), log);
		}
		else
		{
			tf_printf("Checkpoint: %s.\n", sqlite3_errmsg(db));
		}
	}
	return SQLITE_OK;
}

static void _tf_ssb_db_init_internal(sqlite3* db)
{
	sqlite3_extended_result_codes(db, 1);
	_tf_ssb_db_exec(db, "PRAGMA journal_mode = WAL");
	_tf_ssb_db_exec(db, "PRAGMA synchronous = NORMAL");
	sqlite3_busy_handler(db, _tf_ssb_db_busy_handler, db);
}

void tf_ssb_db_init_reader(sqlite3* db)
{
	_tf_ssb_db_init_internal(db);
}

void tf_ssb_db_init(tf_ssb_t* ssb)
{
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	_tf_ssb_db_init_internal(db);
	sqlite3_wal_hook(db, _tf_ssb_db_wal_hook, NULL);

	sqlite3_stmt* statement = NULL;
	int auto_vacuum = 0;
	if (sqlite3_prepare_v2(db, "PRAGMA auto_vacuum", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_step(statement) == SQLITE_ROW)
		{
			auto_vacuum = sqlite3_column_int(statement, 0);
		}
		sqlite3_finalize(statement);
	}
	if (auto_vacuum != 1 /* FULL */)
	{
		tf_printf("Enabling auto-vacuum and performing full vacuum.\n");
		_tf_ssb_db_exec(db, "PRAGMA auto_vacuum = FULL");
		_tf_ssb_db_exec(db, "VACUUM main");
		tf_printf("All clean.\n");
	}

	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS messages ("
		"  author TEXT,"
		"  id TEXT PRIMARY KEY,"
		"  sequence INTEGER,"
		"  timestamp REAL,"
		"  previous TEXT,"
		"  hash TEXT,"
		"  content BLOB,"
		"  signature TEXT,"
		"  flags INTEGER,"
		"  UNIQUE(author, sequence)"
		")");
	if (_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
	{
		if (_tf_ssb_db_has_rows(db, "SELECT 1 FROM messages_stats WHERE max_sequence IS NULL LIMIT 1"))
		{
			tf_printf("Rebuilding messages_stats.\n");
			_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
		}
		else if (!_tf_ssb_db_has_rows(db, "SELECT name FROM pragma_table_info('messages_stats') WHERE name = 'size'"))
		{
			tf_printf("Rebuilding messages_stats.\n");
			_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
		}
	}
	if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
	{
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db,
			"CREATE TABLE IF NOT EXISTS messages_stats ("
			"  author TEXT PRIMARY KEY,"
			"  max_sequence INTEGER NOT NULL,"
			"  max_timestamp REAL NOT NULL,"
			"  size INTEGER NOT NULL DEFAULT 0"
			")");
		_tf_ssb_db_exec(db,
			"INSERT OR REPLACE INTO messages_stats (author, max_sequence, max_timestamp, size) SELECT author, MAX(sequence), MAX(timestamp), SUM(length(json(content))) FROM "
			"messages GROUP BY author");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
	}
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ai_stats AFTER INSERT ON messages BEGIN INSERT INTO messages_stats(author, max_sequence, max_timestamp, size) VALUES (new.author, "
		"new.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, new.sequence), max_timestamp = MAX(max_timestamp, "
		"new.timestamp), size = size + length(json(new.content)); END");
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_stats");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ad_stats AFTER DELETE ON messages BEGIN "
		"UPDATE messages_stats SET max_sequence = updated.sequence, max_timestamp = updated.timestamp, size = size - length(json(old.content)) "
		"FROM ("
		"  SELECT COALESCE(MAX(messages.sequence), 0) AS sequence, COALESCE(MAX(messages.timestamp), 0) AS timestamp "
		"  FROM messages WHERE messages.author = old.author) AS updated "
		"WHERE messages_stats.author = old.author; "
		"DELETE FROM messages_stats WHERE messages_stats.author = old.author AND messages_stats.max_sequence = 0; "
		"END");

	if (_tf_ssb_db_has_rows(db, "SELECT name FROM pragma_table_info('messages') WHERE name = 'content' AND type == 'TEXT'"))
	{
		tf_printf("converting to JSONB\n");
		_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_refs");
		_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_refs");
		_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai");
		_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad");
		_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS messages_fts");
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db, "ALTER TABLE messages ADD COLUMN contentb BLOB");
		_tf_ssb_db_exec(db, "UPDATE messages SET contentb = jsonb(content)");
		_tf_ssb_db_exec(db, "ALTER TABLE messages DROP COLUMN content");
		_tf_ssb_db_exec(db, "ALTER TABLE messages RENAME COLUMN contentb TO content");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
	}

	if (_tf_ssb_db_has_rows(db, "SELECT name FROM pragma_table_info('messages') WHERE name = 'sequence_before_author'"))
	{
		tf_printf("Renaming sequence_before_author -> flags.\n");
		_tf_ssb_db_exec(db, "ALTER TABLE messages RENAME COLUMN sequence_before_author TO flags");
	}

	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_sequence_index ON messages (author, sequence)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_channel_author_timestamp_root_index ON messages (content ->> 'channel', author, timestamp, content ->> 'root')");
	_tf_ssb_db_exec(db,
		"CREATE INDEX IF NOT EXISTS messages_contact_index ON messages(author, sequence, content ->> '$.contact', content ->> '$.following', content ->> '$.blocking') WHERE "
		"content ->> '$.type' = 'contact'");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_id_author_index ON messages (id, author)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_size_by_author_index ON messages (author, length(content))");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_type_root_index ON messages (author, content ->> 'type', content ->> 'root')");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_author_id_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_by_author_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_id_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_timestamp_author_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_type_author_channel_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_type_author_channel_root_rowid_index");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_type_author_channel_root_timestamp_index ");
	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS blobs ("
		"  id TEXT PRIMARY KEY,"
		"  content BLOB,"
		"  created INTEGER"
		")");
	_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS blob_wants");
	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS properties ("
		"  id TEXT,"
		"  key TEXT,"
		"  value TEXT,"
		"  UNIQUE(id, key)"
		")");
	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS connections ("
		"  host TEXT,"
		"  port INTEGER,"
		"  key TEXT,"
		"  last_attempt INTEGER,"
		"  last_success INTEGER,"
		"  UNIQUE(host, port, key)"
		")");
	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS identities ("
		"  user TEXT,"
		"  public_key TEXT UNIQUE,"
		"  private_key TEXT UNIQUE"
		")");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)");
	_tf_ssb_db_exec(db, "DELETE FROM identities WHERE user = ':auth'");

	_tf_ssb_db_exec(db,
		"CREATE TABLE IF NOT EXISTS invites ("
		"  invite_public_key TEXT PRIMARY KEY,"
		"  account TEXT,"
		"  use_count INTEGER,"
		"  expires INTEGER"
		")");

	bool populate_fts = false;
	if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
	{
		_tf_ssb_db_exec(db, "CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, content=messages, content_rowid=rowid)");
		populate_fts = true;
	}

	if (!populate_fts && /* HACK */ false)
	{
		tf_printf("Checking FTS5 integrity...\n");
		if (sqlite3_exec(db, "INSERT INTO messages_fts(messages_fts, rank) VALUES ('integrity-check', 0)", NULL, NULL, NULL) == SQLITE_CORRUPT_VTAB)
		{
			populate_fts = true;
		}
		tf_printf("Done.\n");
	}

	if (populate_fts)
	{
		tf_printf("Populating full-text search...\n");
		_tf_ssb_db_exec(db, "INSERT INTO messages_fts (rowid, content) SELECT rowid, json(content) FROM messages");
		tf_printf("Done.\n");
	}

	_tf_ssb_db_exec(
		db, "CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, json(new.content)); END");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.rowid, "
		"old.content); END");

	if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%ltrim%'"))
	{
		tf_printf("Deleting incorrect messages_refs...\n");
		_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS messages_refs");
		tf_printf("Done.\n");
	}

	if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_refs')"))
	{
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db,
			"CREATE TABLE IF NOT EXISTS messages_refs ("
			"  message TEXT, "
			"  ref TEXT, "
			"  UNIQUE(message, ref)"
			")");
		tf_printf("Populating messages_refs...\n");
		_tf_ssb_db_exec(db,
			"INSERT INTO messages_refs(message, ref) "
			"SELECT messages.id, j.value FROM messages, json_tree(messages.content) AS j WHERE "
			"j.value LIKE '&%.sha256' OR "
			"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
			"j.value LIKE '@%.ed25519' OR "
			"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
			"ON CONFLICT DO NOTHING");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
		tf_printf("Done.\n");
	}

	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_refs");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ai_refs AFTER INSERT ON messages BEGIN "
		"INSERT INTO messages_refs(message, ref) "
		"SELECT DISTINCT new.id, j.value FROM json_tree(new.content) AS j WHERE "
		"j.value LIKE '&%.sha256' OR "
		"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
		"j.value LIKE '@%.ed25519' OR "
		"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
		"ON CONFLICT DO NOTHING; END");
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_refs");
	_tf_ssb_db_exec(db, "CREATE TRIGGER IF NOT EXISTS messages_ad_refs AFTER DELETE ON messages BEGIN DELETE FROM messages_refs WHERE messages_refs.message = old.id; END");

	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_refs_message_idx");
	_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_refs_ref_idx");
	_tf_ssb_db_exec(db, "CREATE UNIQUE INDEX IF NOT EXISTS messages_refs_message_ref_idx ON messages_refs (message, ref)");
	_tf_ssb_db_exec(db, "CREATE UNIQUE INDEX IF NOT EXISTS messages_refs_ref_message_idx ON messages_refs (ref, message)");

	if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('blobs_refs')"))
	{
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db,
			"CREATE TABLE IF NOT EXISTS blobs_refs ("
			"  blob TEXT, "
			"  ref TEXT, "
			"  UNIQUE(blob, ref)"
			")");
		tf_printf("Populating blobs_refs...\n");
		_tf_ssb_db_exec(db,
			"INSERT INTO blobs_refs(blob, ref) "
			"SELECT blobs.id, j.value FROM blobs, json_tree(blobs.content) AS j WHERE "
			"json_valid(blobs.content) AND j.value LIKE '&%.sha256' "
			"ON CONFLICT DO NOTHING");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
		tf_printf("Done.\n");
	}
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS blobs_ai_refs");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS blobs_ai_refs AFTER INSERT ON blobs BEGIN "
		"INSERT INTO blobs_refs(blob, ref) "
		"SELECT DISTINCT new.id, j.value FROM json_tree(new.content) AS j WHERE "
		"json_valid(new.content) AND j.value LIKE '&%.sha256' "
		"ON CONFLICT DO NOTHING; END");
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS blobs_ad_refs");
	_tf_ssb_db_exec(db, "CREATE TRIGGER IF NOT EXISTS blobs_ad_refs AFTER DELETE ON blobs BEGIN DELETE FROM blobs_refs WHERE blobs_refs.blob = old.id; END");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blobs_refs_blob_idx ON blobs_refs (blob)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blobs_refs_ref_idx ON blobs_refs (ref)");

	_tf_ssb_db_exec(db, "DROP VIEW IF EXISTS blob_wants_view");
	_tf_ssb_db_exec(db,
		"CREATE VIEW IF NOT EXISTS blob_wants_view (source, id, timestamp) AS "
		"  WITH RECURSIVE "
		"  wanted1 AS ( "
		"    SELECT messages_refs.message AS source, messages_refs.ref AS id, messages.timestamp AS timestamp "
		"    FROM messages_refs "
		"    JOIN messages ON messages.id = messages_refs.message "
		"    UNION "
		"    SELECT messages_refs.message AS source, messages_refs.ref AS id, unixepoch() * 1000 AS timestamp "
		"    FROM messages_refs "
		"    JOIN messages ON messages.id = messages_refs.message "
		"    WHERE messages.content ->> 'type' = 'about' "
		"  ), "
		"  wanted(source, id, timestamp) AS ( "
		"    SELECT wanted1.source AS source, wanted1.id AS id, wanted1.timestamp AS timestamp FROM wanted1 "
		"    UNION "
		"    SELECT wanted.source AS source, br.ref AS id, wanted.timestamp AS timestamp FROM wanted JOIN blobs_refs br ON br.blob = wanted.id "
		"  ) "
		"  SELECT wanted.source, wanted.id, wanted.timestamp FROM wanted "
		"  LEFT OUTER JOIN blobs ON wanted.id = blobs.id "
		"  WHERE blobs.id IS NULL "
		"    AND LENGTH(wanted.id) = 52 "
		"    AND wanted.id LIKE '&%.sha256'");
	if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('blob_wants_cache')"))
	{
		tf_printf("Populating blob_wants_cache...\n");
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db, "CREATE TABLE IF NOT EXISTS blob_wants_cache (source TEXT, id TEXT, timestamp REAL, UNIQUE(source, id))");
		_tf_ssb_db_exec(
			db, "INSERT INTO blob_wants_cache SELECT * FROM blob_wants_view WHERE true ON CONFLICT(source, id) DO UPDATE SET timestamp = MAX(timestamp, excluded.timestamp)");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
		tf_printf("Done.\n");
	}
	_tf_ssb_db_exec(db, "DELETE FROM blob_wants_cache WHERE blob_wants_cache.id IN (SELECT blobs.id FROM blobs)");
	if (!_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'index' AND name = 'blob_wants_cache_source_id_unique_index'"))
	{
		tf_printf("Creating blob_wants_cache UNIQUE constraint.\n");
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db, "DELETE FROM blob_wants_cache WHERE source IS NULL");
		_tf_ssb_db_exec(db, "CREATE UNIQUE INDEX blob_wants_cache_source_id_unique_index ON blob_wants_cache (COALESCE(source, ''), id)");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
		tf_printf("Done.\n");
	}
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_id_idx ON blob_wants_cache (id)");
	_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_timestamp_id_idx ON blob_wants_cache (timestamp, id)");

	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ai_blob_wants_cache AFTER INSERT ON messages_refs BEGIN "
		"INSERT INTO blob_wants_cache (source, id, timestamp) "
		"SELECT messages.id, new.ref, messages.timestamp FROM messages "
		"JOIN blobs ON new.ref = blobs.id "
		"WHERE messages.id = new.message AND "
		"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' AND "
		"blobs.content IS NULL "
		"ON CONFLICT (source, id) DO NOTHING; END");
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS blobs_refs_ai_blob_wants_cache");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS blobs_refs_ai_blob_wants_cache AFTER INSERT ON blobs_refs BEGIN "
		"INSERT INTO blob_wants_cache (source, id, timestamp) "
		"SELECT messages.id, new.ref, messages.timestamp FROM messages "
		"JOIN blob_wants_cache bwc ON bwc.source = messages.id AND bwc.id = new.blob "
		"JOIN blobs ON bwc.id = blobs.id "
		"WHERE blobs.content IS NULL "
		"ON CONFLICT (source, id) DO NOTHING; END");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS messages_ad_blob_wants_cache AFTER DELETE ON messages BEGIN "
		"DELETE FROM blob_wants_cache WHERE blob_wants_cache.source = old.id; END");
	_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS blobs_ai_blob_wants_cache");
	_tf_ssb_db_exec(db,
		"CREATE TRIGGER IF NOT EXISTS blobs_ai_blob_wants_cache AFTER INSERT ON blobs BEGIN "
		"DELETE FROM blob_wants_cache WHERE blob_wants_cache.id = new.id; END");

	bool need_add_flags = true;
	bool need_convert_timestamp_to_real = false;

	if (sqlite3_prepare_v2(db, "PRAGMA table_info(messages)", -1, &statement, NULL) == SQLITE_OK)
	{
		int result = SQLITE_OK;
		while ((result = sqlite3_step(statement)) == SQLITE_ROW)
		{
			const char* name = (const char*)sqlite3_column_text(statement, 1);
			const char* type = (const char*)sqlite3_column_text(statement, 2);
			if (name && type && strcmp(name, "timestamp") == 0 && strcmp(type, "INTEGER") == 0)
			{
				need_convert_timestamp_to_real = true;
			}
			if (name && strcmp(name, "flags") == 0)
			{
				need_add_flags = false;
			}
		}
		sqlite3_finalize(statement);
	}

	if (need_convert_timestamp_to_real)
	{
		tf_printf("Converting timestamp column from INTEGER to REAL.\n");
		_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
		_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_author_timestamp_index");
		_tf_ssb_db_exec(db, "ALTER TABLE messages ADD COLUMN timestamp_real REAL");
		_tf_ssb_db_exec(db, "UPDATE messages SET timestamp_real = timestamp");
		_tf_ssb_db_exec(db, "ALTER TABLE messages DROP COLUMN timestamp");
		_tf_ssb_db_exec(db, "ALTER TABLE messages RENAME COLUMN timestamp_real TO timestamp");
		_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)");
		_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
	}
	if (need_add_flags)
	{
		tf_printf("Adding flags column.\n");
		_tf_ssb_db_exec(db, "ALTER TABLE messages ADD COLUMN flags INTEGER");
	}
	tf_ssb_release_db_writer(ssb, db);
}

static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int32_t sequence, const char* previous, bool* out_id_mismatch)
{
	bool exists = false;
	if (sequence == 1)
	{
		exists = true;
	}
	else
	{
		sqlite3_stmt* statement;
		if (sqlite3_prepare_v2(db, "SELECT COUNT(*), id != ?3 AS is_mismatch FROM messages WHERE author = ?1 AND sequence = ?2", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, sequence - 1) == SQLITE_OK &&
				sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
			{
				exists = sqlite3_column_int(statement, 0) != 0;
				*out_id_mismatch = sqlite3_column_int(statement, 1) != 0;
			}
			sqlite3_finalize(statement);
		}
	}
	return exists;
}

static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp, const char* content,
	size_t content_len, const char* signature, int flags)
{
	int64_t last_row_id = -1;
	bool id_mismatch = false;

	if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
	{
		const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
							"?, ?, ?) ON CONFLICT DO NOTHING";
		sqlite3_stmt* statement;
		if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
				(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
				sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
				sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
				sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
				sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
			{
				int r = sqlite3_step(statement);
				if (r != SQLITE_DONE)
				{
					tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
				}
				if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
				{
					last_row_id = sqlite3_last_insert_rowid(db);
				}
			}
			else
			{
				tf_printf("bind failed\n");
			}
			sqlite3_finalize(statement);
		}
		else
		{
			tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
		}
	}
	else if (id_mismatch)
	{
		/*
		** Only warn if we find a previous message with the wrong ID.
		** If a feed is forked, we would otherwise warn on every
		** message when trying to receive what we don't have, and
		** that's not helping anybody.
		*/
		tf_printf("%p: Previous message doesn't exist for author=%s sequence=%d previous=%s.\n", db, author, sequence, previous);
	}
	return last_row_id;
}

static char* _tf_ssb_db_get_message_blob_wants(sqlite3* db, int64_t rowid)
{
	sqlite3_stmt* statement;
	char* result = NULL;
	size_t size = 0;

	if (sqlite3_prepare_v2(db,
			"SELECT DISTINCT json.value FROM messages, json_tree(messages.content) AS json LEFT OUTER JOIN blobs ON json.value = blobs.id WHERE messages.rowid = ?1 AND "
			"length(json.value) = ?2 AND json.value LIKE '&%.sha256' AND blobs.content IS NULL",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_int64(statement, 1, rowid) == SQLITE_OK && sqlite3_bind_int(statement, 2, k_blob_id_len - 1) == SQLITE_OK)
		{
			int r = SQLITE_OK;
			while ((r = sqlite3_step(statement)) == SQLITE_ROW)
			{
				int id_size = sqlite3_column_bytes(statement, 0);
				const uint8_t* id = sqlite3_column_text(statement, 0);
				result = tf_resize_vec(result, size + id_size + 1);
				memcpy(result + size, id, id_size + 1);
				size += id_size + 1;
			}
			if (r != SQLITE_DONE)
			{
				tf_printf("%s\n", sqlite3_errmsg(db));
			}
		}
		else
		{
			tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
	}
	result = tf_realloc(result, size + 1);
	result[size] = '\0';

	return result;
}

typedef struct _message_store_t
{
	char id[k_id_base64_len];
	char signature[512];
	int flags;
	char previous[k_id_base64_len];
	char author[k_id_base64_len];
	int32_t sequence;
	double timestamp;
	const char* content;
	size_t length;

	bool out_stored;
	char* out_blob_wants;

	tf_ssb_db_store_message_callback_t* callback;
	void* user_data;

	message_store_t* next;
} message_store_t;

static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
{
	message_store_t* store = user_data;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	bool in_transaction = _tf_ssb_db_try_exec(db, "BEGIN TRANSACTION") == SQLITE_OK;

	while (store)
	{
		int64_t last_row_id = _tf_ssb_db_store_message_raw(db, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp,
			store->content, store->length, store->signature, store->flags);
		if (last_row_id != -1)
		{
			store->out_stored = true;
			store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
		}
		store = store->next;
	}

	if (in_transaction)
	{
		if (_tf_ssb_db_try_exec(db, "COMMIT TRANSACTION") != SQLITE_OK)
		{
			store = user_data;
			while (store)
			{
				store->out_stored = false;
				store = store->next;
			}
		}
	}
	tf_ssb_release_db_writer(ssb, db);
}

static void _wake_up_queue(tf_ssb_t* ssb, tf_ssb_store_queue_t* queue)
{
	if (!queue->running)
	{
		message_store_t* stores = queue->head;
		queue->head = NULL;
		queue->tail = NULL;
		if (stores)
		{
			queue->running = true;
			tf_ssb_run_work(ssb, _tf_ssb_db_store_message_work, _tf_ssb_db_store_message_after_work, stores);
		}
	}
}

static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	message_store_t* store = user_data;
	tf_trace_t* trace = tf_ssb_get_trace(ssb);

	message_store_t* last_stored = NULL;

	while (store)
	{
		if (store->out_stored)
		{
			last_stored = store;
		}
		if (store->out_blob_wants)
		{
			tf_trace_begin(trace, "notify_blob_wants_added");
			for (char* p = store->out_blob_wants; *p; p = p + strlen(p))
			{
				tf_ssb_notify_blob_want_added(ssb, p);
			}
			tf_free(store->out_blob_wants);
			tf_trace_end(trace);
		}

		if (store->callback)
		{
			store->callback(store->id, store->out_stored, store->user_data);
		}
		store = store->next;
	}

	if (last_stored)
	{
		tf_trace_begin(trace, "notify_message_added");
		JSContext* context = tf_ssb_get_context(ssb);
		JSValue formatted = tf_ssb_format_message(context, last_stored->previous, last_stored->author, last_stored->sequence, last_stored->timestamp, "sha256",
			last_stored->content, last_stored->signature, last_stored->flags);
		JSValue message = JS_NewObject(context);
		JS_SetPropertyStr(context, message, "key", JS_NewString(context, last_stored->id));
		JS_SetPropertyStr(context, message, "value", formatted);
		char timestamp_string[256];
		snprintf(timestamp_string, sizeof(timestamp_string), "%f", last_stored->timestamp);
		JS_SetPropertyStr(context, message, "timestamp", JS_NewString(context, timestamp_string));
		tf_ssb_notify_message_added(ssb, last_stored->author, last_stored->sequence, last_stored->id, message);
		JS_FreeValue(context, message);
		tf_trace_end(trace);
	}

	JSContext* context = tf_ssb_get_context(ssb);
	store = user_data;
	while (store)
	{
		JS_FreeCString(context, store->content);
		message_store_t* last = store;
		store = store->next;
		tf_free(last);
	}

	tf_ssb_store_queue_t* queue = tf_ssb_get_store_queue(ssb);
	queue->running = false;
	_wake_up_queue(ssb, queue);
}

void tf_ssb_db_store_message(
	tf_ssb_t* ssb, JSContext* context, const char* id, JSValue val, const char* signature, int flags, tf_ssb_db_store_message_callback_t* callback, void* user_data)
{
	JSValue previousval = JS_GetPropertyStr(context, val, "previous");
	const char* previous = JS_IsNull(previousval) ? NULL : JS_ToCString(context, previousval);
	JS_FreeValue(context, previousval);

	JSValue authorval = JS_GetPropertyStr(context, val, "author");
	const char* author = JS_ToCString(context, authorval);
	JS_FreeValue(context, authorval);

	int32_t sequence = -1;
	JSValue sequenceval = JS_GetPropertyStr(context, val, "sequence");
	JS_ToInt32(context, &sequence, sequenceval);
	JS_FreeValue(context, sequenceval);

	double timestamp = -1.0;
	JSValue timestampval = JS_GetPropertyStr(context, val, "timestamp");
	JS_ToFloat64(context, &timestamp, timestampval);
	JS_FreeValue(context, timestampval);

	JSValue contentval = JS_GetPropertyStr(context, val, "content");
	JSValue content = JS_JSONStringify(context, contentval, JS_NULL, JS_NULL);
	size_t content_len;
	const char* contentstr = JS_ToCStringLen(context, &content_len, content);
	JS_FreeValue(context, content);
	JS_FreeValue(context, contentval);

	message_store_t* store = tf_malloc(sizeof(message_store_t));
	*store = (message_store_t) {
		.sequence = sequence,
		.timestamp = timestamp,
		.content = contentstr,
		.length = content_len,
		.flags = flags,

		.callback = callback,
		.user_data = user_data,
	};
	tf_string_set(store->id, sizeof(store->id), id);
	tf_string_set(store->previous, sizeof(store->previous), previous ? previous : "");
	tf_string_set(store->author, sizeof(store->author), author);
	tf_string_set(store->signature, sizeof(store->signature), signature);
	JS_FreeCString(context, author);
	JS_FreeCString(context, previous);

	tf_ssb_store_queue_t* queue = tf_ssb_get_store_queue(ssb);
	if (queue->tail)
	{
		message_store_t* tail = queue->tail;
		tail->next = store;
		queue->tail = store;
	}
	else
	{
		queue->head = store;
		queue->tail = store;
	}
	_wake_up_queue(ssb, queue);
}

bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size)
{
	bool result = false;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	const char* query = "SELECT json(content) FROM messages WHERE id = ?";
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			const uint8_t* blob = sqlite3_column_blob(statement, 0);
			int size = sqlite3_column_bytes(statement, 0);
			if (out_blob)
			{
				*out_blob = tf_malloc(size + 1);
				memcpy(*out_blob, blob, size);
				(*out_blob)[size] = '\0';
			}
			if (out_size)
			{
				*out_size = size;
			}
			result = true;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

bool tf_ssb_db_blob_has(sqlite3* db, const char* id)
{
	bool result = false;
	sqlite3_stmt* statement;
	const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1";
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			result = sqlite3_column_int64(statement, 0) != 0;
		}
		sqlite3_finalize(statement);
	}
	return result;
}

bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size)
{
	bool result = false;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	const char* query = "SELECT content FROM blobs WHERE id = ?1";
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			const uint8_t* blob = sqlite3_column_blob(statement, 0);
			int size = sqlite3_column_bytes(statement, 0);
			if (out_blob)
			{
				*out_blob = tf_malloc(size + 1);
				if (size)
				{
					memcpy(*out_blob, blob, size);
				}
				(*out_blob)[size] = '\0';
			}
			if (out_size)
			{
				*out_size = size;
			}
			result = true;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

void tf_ssb_db_add_blob_wants(sqlite3* db, const char* id)
{
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO blob_wants_cache (id, timestamp) VALUES (?, unixepoch() * 1000) ON CONFLICT DO UPDATE SET timestamp = excluded.timestamp",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) != SQLITE_DONE)
			{
				tf_printf("blob wants cache update failed: %s.\n", sqlite3_errmsg(db));
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
}

typedef struct _blob_get_async_t
{
	tf_ssb_t* ssb;
	char id[k_blob_id_len];
	tf_ssb_db_blob_get_callback_t* callback;
	void* user_data;

	bool out_found;
	uint8_t* out_data;
	size_t out_size;
} blob_get_async_t;

static void _tf_ssb_db_blob_get_async_work(tf_ssb_t* ssb, void* user_data)
{
	blob_get_async_t* async = user_data;
	async->out_found = tf_ssb_db_blob_get(ssb, async->id, &async->out_data, &async->out_size);
}

static void _tf_ssb_db_blob_get_async_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	blob_get_async_t* async = user_data;
	async->callback(async->out_found, async->out_data, async->out_size, async->user_data);
	tf_free(async->out_data);
	tf_free(async);
}

void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_callback_t* callback, void* user_data)
{
	blob_get_async_t* async = tf_malloc(sizeof(blob_get_async_t));
	*async = (blob_get_async_t) {
		.ssb = ssb,
		.callback = callback,
		.user_data = user_data,
	};
	tf_string_set(async->id, sizeof(async->id), id);
	tf_ssb_run_work(ssb, _tf_ssb_db_blob_get_async_work, _tf_ssb_db_blob_get_async_after_work, async);
}

typedef struct _blob_store_work_t
{
	const uint8_t* blob;
	size_t size;
	char id[k_blob_id_len];
	bool is_new;
	tf_ssb_db_blob_store_callback_t* callback;
	void* user_data;
} blob_store_work_t;

static void _tf_ssb_db_blob_store_work(tf_ssb_t* ssb, void* user_data)
{
	blob_store_work_t* blob_work = user_data;
	if (!tf_ssb_is_shutting_down(ssb))
	{
		tf_ssb_db_blob_store(ssb, blob_work->blob, blob_work->size, blob_work->id, sizeof(blob_work->id), &blob_work->is_new);
	}
}

static void _tf_ssb_db_blob_store_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	blob_store_work_t* blob_work = user_data;
	if (status == 0 && *blob_work->id && blob_work->is_new)
	{
		tf_ssb_notify_blob_stored(ssb, blob_work->id);
	}
	if (status != 0)
	{
		tf_printf("tf_ssb_db_blob_store_async -> uv_queue_work failed asynchronously: %s\n", uv_strerror(status));
	}
	if (blob_work->callback)
	{
		blob_work->callback(status == 0 ? blob_work->id : NULL, blob_work->is_new, blob_work->user_data);
	}
	tf_free(blob_work);
}

void tf_ssb_db_blob_store_async(tf_ssb_t* ssb, const uint8_t* blob, size_t size, tf_ssb_db_blob_store_callback_t* callback, void* user_data)
{
	blob_store_work_t* work = tf_malloc(sizeof(blob_store_work_t));
	*work = (blob_store_work_t) {
		.blob = blob,
		.size = size,
		.callback = callback,
		.user_data = user_data,
	};
	tf_ssb_run_work(ssb, _tf_ssb_db_blob_store_work, _tf_ssb_db_blob_store_after_work, work);
}

bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char* out_id, size_t out_id_size, bool* out_new)
{
	bool result = false;

	uint8_t hash[crypto_hash_sha256_BYTES];
	crypto_hash_sha256(hash, blob, size);

	char hash64[256];
	tf_base64_encode(hash, sizeof(hash), hash64, sizeof(hash64));

	char id[512];
	snprintf(id, sizeof(id), "&%s.sha256", hash64);

	int rows = 0;

	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "INSERT INTO blobs (id, content, created) VALUES (?1, ?2, CAST(strftime('%s') AS INTEGER)) ON CONFLICT DO NOTHING", -1, &statement, NULL) ==
		SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_blob(statement, 2, blob, size, NULL) == SQLITE_OK)
		{
			int r = sqlite3_step(statement);
			result = r == SQLITE_DONE;
			if (!result)
			{
				tf_printf("Blob store failed (%s: %zd): %s / %s.\n", id, size, sqlite3_errstr(r), sqlite3_errmsg(db));
			}
			rows = sqlite3_changes(db);
		}
		else
		{
			tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
	}
	tf_ssb_release_db_writer(ssb, db);

	if (result && out_id)
	{
		tf_string_set(out_id, out_id_size, id);
	}
	if (out_new)
	{
		*out_new = rows != 0;
	}
	return result;
}

bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int32_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
	size_t out_previous_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, size_t out_signature_size, int* out_flags)
{
	bool found = false;
	sqlite3_stmt* statement;
	const char* query = "SELECT id, previous, timestamp, json(content), hash, signature, flags FROM messages WHERE author = ?1 AND sequence = ?2";
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, sequence) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			if (out_message_id)
			{
				tf_string_set(out_message_id, out_message_id_size, (const char*)sqlite3_column_text(statement, 0));
			}
			if (out_previous)
			{
				if (sqlite3_column_type(statement, 1) == SQLITE_NULL)
				{
					if (out_previous_size)
					{
						*out_previous = '\0';
					}
				}
				else
				{
					tf_string_set(out_previous, out_previous_size, (const char*)sqlite3_column_text(statement, 1));
				}
			}
			if (out_timestamp)
			{
				*out_timestamp = sqlite3_column_double(statement, 2);
			}
			if (out_content)
			{
				*out_content = tf_strdup((const char*)sqlite3_column_text(statement, 3));
			}
			if (out_hash)
			{
				tf_string_set(out_hash, out_hash_size, (const char*)sqlite3_column_text(statement, 4));
			}
			if (out_signature)
			{
				tf_string_set(out_signature, out_signature_size, (const char*)sqlite3_column_text(statement, 5));
			}
			if (out_flags)
			{
				*out_flags = sqlite3_column_int(statement, 6);
			}
			found = true;
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
	}
	tf_ssb_release_db_reader(ssb, db);
	return found;
}

bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, int32_t* out_sequence, char* out_message_id, size_t out_message_id_size)
{
	bool found = false;
	sqlite3_stmt* statement;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);

	if (out_message_id)
	{
		const char* query = "SELECT id, sequence FROM messages WHERE author = ?1 ORDER BY sequence DESC LIMIT 1";
		if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
			{
				if (out_sequence)
				{
					*out_sequence = sqlite3_column_int(statement, 1);
				}
				if (out_message_id)
				{
					strncpy(out_message_id, (const char*)sqlite3_column_text(statement, 0), out_message_id_size - 1);
				}
				found = true;
			}
			sqlite3_finalize(statement);
		}
		else
		{
			tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
		}
	}
	else
	{
		const char* query = "SELECT max_sequence FROM messages_stats WHERE author = ?1";
		if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
			{
				if (out_sequence)
				{
					*out_sequence = sqlite3_column_int(statement, 0);
				}
				found = true;
			}
			sqlite3_finalize(statement);
		}
		else
		{
			tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
		}
	}

	tf_ssb_release_db_reader(ssb, db);
	return found;
}

static JSValue _tf_ssb_sqlite_bind_json(JSContext* context, sqlite3* db, sqlite3_stmt* statement, JSValue binds)
{
	if (JS_IsUndefined(binds))
	{
		return JS_UNDEFINED;
	}
	if (!JS_IsArray(context, binds))
	{
		return JS_ThrowTypeError(context, "Expected bind parameters to be an array.");
	}

	JSValue result = JS_UNDEFINED;
	int32_t length = tf_util_get_length(context, binds);
	for (int i = 0; i < length && JS_IsUndefined(result); i++)
	{
		JSValue value = JS_GetPropertyUint32(context, binds, i);
		if (JS_IsNumber(value))
		{
			int64_t number = 0;
			JS_ToInt64(context, &number, value);
			if (sqlite3_bind_int64(statement, i + 1, number) != SQLITE_OK)
			{
				result = JS_ThrowInternalError(context, "Failed to bind: %s.", sqlite3_errmsg(db));
			}
		}
		else if (JS_IsBool(value))
		{
			if (sqlite3_bind_int(statement, i + 1, JS_ToBool(context, value) ? 1 : 0) != SQLITE_OK)
			{
				result = JS_ThrowInternalError(context, "Failed to bind: %s.", sqlite3_errmsg(db));
			}
		}
		else if (JS_IsNull(value))
		{
			if (sqlite3_bind_null(statement, i + 1) != SQLITE_OK)
			{
				result = JS_ThrowInternalError(context, "Failed to bind: %s.", sqlite3_errmsg(db));
			}
		}
		else
		{
			size_t str_len = 0;
			const char* str = JS_ToCStringLen(context, &str_len, value);
			if (str)
			{
				if (sqlite3_bind_text(statement, i + 1, str, str_len, SQLITE_TRANSIENT) != SQLITE_OK)
				{
					result = JS_ThrowInternalError(context, "Failed to bind: %s.", sqlite3_errmsg(db));
				}
				JS_FreeCString(context, str);
			}
			else
			{
				result = JS_ThrowInternalError(context, "Could not convert bind argument %d to string.", i);
			}
		}
		JS_FreeValue(context, value);
	}
	return result;
}

static JSValue _tf_ssb_sqlite_row_to_json(JSContext* context, sqlite3_stmt* row)
{
	JSValue result = JS_NewObject(context);
	for (int i = 0; i < sqlite3_column_count(row); i++)
	{
		const char* name = sqlite3_column_name(row, i);
		switch (sqlite3_column_type(row, i))
		{
		case SQLITE_INTEGER:
			JS_SetPropertyStr(context, result, name, JS_NewInt64(context, sqlite3_column_int64(row, i)));
			break;
		case SQLITE_FLOAT:
			JS_SetPropertyStr(context, result, name, JS_NewFloat64(context, sqlite3_column_double(row, i)));
			break;
		case SQLITE_TEXT:
			JS_SetPropertyStr(context, result, name, JS_NewStringLen(context, (const char*)sqlite3_column_text(row, i), sqlite3_column_bytes(row, i)));
			break;
		case SQLITE_BLOB:
			JS_SetPropertyStr(context, result, name, JS_NewArrayBufferCopy(context, sqlite3_column_blob(row, i), sqlite3_column_bytes(row, i)));
			break;
		case SQLITE_NULL:
			JS_SetPropertyStr(context, result, name, JS_NULL);
			break;
		}
	}
	return result;
}

int tf_ssb_sqlite_authorizer(void* user_data, int action_code, const char* arg0, const char* arg1, const char* arg2, const char* arg3)
{
	int result = SQLITE_DENY;
	switch (action_code)
	{
	case SQLITE_SELECT:
	case SQLITE_FUNCTION:
		result = SQLITE_OK;
		break;
	case SQLITE_READ:
		result = (strcmp(arg0, "blob_wants_cache") == 0 || strcmp(arg0, "blob_wants_view") == 0 || strcmp(arg0, "json_each") == 0 || strcmp(arg0, "json_tree") == 0 ||
					 strcmp(arg0, "messages") == 0 || strcmp(arg0, "messages_stats") == 0 || strcmp(arg0, "messages_fts") == 0 || strcmp(arg0, "messages_fts_idx") == 0 ||
					 strcmp(arg0, "messages_fts_config") == 0 || strcmp(arg0, "messages_refs") == 0 || strcmp(arg0, "messages_refs_message_idx") == 0 ||
					 strcmp(arg0, "messages_refs_ref_idx") == 0 || strcmp(arg0, "sqlite_master") == 0 || false)
			? SQLITE_OK
			: SQLITE_DENY;
		break;
	case SQLITE_PRAGMA:
		result = strcmp(arg0, "data_version") == 0 ? SQLITE_OK : SQLITE_DENY;
		break;
	case SQLITE_UPDATE:
		result = strcmp(arg0, "sqlite_master") == 0 ? SQLITE_OK : SQLITE_DENY;
		break;
	}
	if (result != SQLITE_OK)
	{
		tf_printf("Denying sqlite access to %d %s %s %s %s\n", action_code, arg0, arg1, arg2, arg3);
		fflush(stdout);
	}
	return result;
}

JSValue tf_ssb_db_visit_query(tf_ssb_t* ssb, const char* query, const JSValue binds, void (*callback)(JSValue row, void* user_data), void* user_data)
{
	JSValue result = JS_UNDEFINED;
	sqlite3* db = tf_ssb_acquire_db_reader_restricted(ssb);
	JSContext* context = tf_ssb_get_context(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
	{
		JSValue bind_result = _tf_ssb_sqlite_bind_json(context, db, statement, binds);
		if (JS_IsUndefined(bind_result))
		{
			int r = SQLITE_OK;
			while ((r = sqlite3_step(statement)) == SQLITE_ROW)
			{
				JSValue row = _tf_ssb_sqlite_row_to_json(context, statement);
				tf_trace_t* trace = tf_ssb_get_trace(ssb);
				tf_trace_begin(trace, "callback");
				callback(row, user_data);
				tf_trace_end(trace);
				JS_FreeValue(context, row);
			}
			if (r != SQLITE_DONE)
			{
				result = JS_ThrowInternalError(context, "SQL Error %s: running \"%s\".", sqlite3_errmsg(db), query);
			}
		}
		else
		{
			result = bind_result;
		}
		sqlite3_finalize(statement);
	}
	else
	{
		result = JS_ThrowInternalError(context, "SQL Error %s: preparing \"%s\".", sqlite3_errmsg(db), query);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

JSValue tf_ssb_format_message(
	JSContext* context, const char* previous, const char* author, int32_t sequence, double timestamp, const char* hash, const char* content, const char* signature, int flags)
{
	JSValue value = JS_NewObject(context);
	JS_SetPropertyStr(context, value, "previous", (previous && *previous) ? JS_NewString(context, previous) : JS_NULL);
	if (flags & k_tf_ssb_message_flag_sequence_before_author)
	{
		JS_SetPropertyStr(context, value, "sequence", JS_NewInt32(context, sequence));
		JS_SetPropertyStr(context, value, "author", JS_NewString(context, author));
	}
	else
	{
		JS_SetPropertyStr(context, value, "author", JS_NewString(context, author));
		JS_SetPropertyStr(context, value, "sequence", JS_NewInt32(context, sequence));
	}
	JS_SetPropertyStr(context, value, "timestamp", JS_NewFloat64(context, timestamp));
	JS_SetPropertyStr(context, value, "hash", JS_NewString(context, hash));
	JS_SetPropertyStr(context, value, "content", JS_ParseJSON(context, content, strlen(content), NULL));
	JS_SetPropertyStr(context, value, "signature", JS_NewString(context, signature));
	return value;
}

int tf_ssb_db_identity_get_count_for_user(tf_ssb_t* ssb, const char* user)
{
	int count = 0;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM identities WHERE user = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				count = sqlite3_column_int(statement, 0);
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return count;
}

bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_public_key, uint8_t* out_private_key)
{
	int count = tf_ssb_db_identity_get_count_for_user(ssb, user);
	if (count < 16)
	{
		char public[512];
		char private[512];
		tf_ssb_generate_keys_buffer(public, sizeof(public), private, sizeof(private));
		if (tf_ssb_db_identity_add(ssb, user, public, private))
		{
			if (out_public_key)
			{
				tf_ssb_id_str_to_bin(out_public_key, public);
			}
			if (out_private_key)
			{
				tf_ssb_id_str_to_bin(out_private_key, private);
				/* HACK: tf_ssb_id_str_to_bin only produces 32 bytes even though the full private key is 32 + 32. */
				tf_ssb_id_str_to_bin(out_private_key + crypto_sign_PUBLICKEYBYTES, public);
			}
			return true;
		}
	}
	return false;
}

bool tf_ssb_db_identity_add(tf_ssb_t* ssb, const char* user, const char* public_key, const char* private_key)
{
	bool added = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "INSERT INTO identities (user, public_key, private_key) VALUES (?, ?, ?) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, public_key, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, private_key, -1, NULL) == SQLITE_OK)
		{
			added = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
			if (!added)
			{
				tf_printf("Unable to add identity: %s.\n", sqlite3_errmsg(db));
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return added;
}

bool tf_ssb_db_identity_delete(tf_ssb_t* ssb, const char* user, const char* public_key)
{
	bool removed = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	tf_printf("deleting [%s] [%s]\n", user, public_key);
	if (sqlite3_prepare_v2(db, "DELETE FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, public_key, -1, NULL) == SQLITE_OK)
		{
			removed = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
			if (!removed)
			{
				tf_printf("Unable to delete identity: %s.\n", sqlite3_errmsg(db));
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return removed;
}

void tf_ssb_db_identity_visit(tf_ssb_t* ssb, const char* user, void (*callback)(const char* identity, void* user_data), void* user_data)
{
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT public_key FROM identities WHERE user = ? ORDER BY public_key", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK)
		{
			while (sqlite3_step(statement) == SQLITE_ROW)
			{
				callback((const char*)sqlite3_column_text(statement, 0), user_data);
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

void tf_ssb_db_identity_visit_all(tf_ssb_t* ssb, void (*callback)(const char* identity, void* user_data), void* user_data)
{
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT public_key FROM identities ORDER BY public_key", -1, &statement, NULL) == SQLITE_OK)
	{
		while (sqlite3_step(statement) == SQLITE_ROW)
		{
			callback((const char*)sqlite3_column_text(statement, 0), user_data);
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
}

const char* tf_ssb_db_get_user_for_identity(tf_ssb_t* ssb, const char* public_key)
{
	const char* result = NULL;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT user FROM identities WHERE public_key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, (public_key && *public_key == '@') ? public_key + 1 : public_key, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
			}
		}
		else
		{
			tf_printf("Bind failed: %s.\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("Prepare failed: %s.\n", sqlite3_errmsg(db));
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const char* public_key, uint8_t* out_private_key, size_t private_key_size)
{
	bool success = false;
	if (out_private_key)
	{
		memset(out_private_key, 0, private_key_size);
	}
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT private_key FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 2, (public_key && *public_key == '@') ? public_key + 1 : public_key, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				const char* key = (const char*)sqlite3_column_text(statement, 0);
				if (out_private_key && private_key_size)
				{
					int r = tf_base64_decode(key, sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), out_private_key, private_key_size);
					success = r > 0;
				}
				else
				{
					success = true;
				}
			}
		}
		else
		{
			tf_printf("Bind failed: %s.\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("Prepare failed: %s.\n", sqlite3_errmsg(db));
	}
	tf_ssb_release_db_reader(ssb, db);
	return success;
}

typedef struct _following_t following_t;

typedef struct _following_t
{
	char id[k_id_base64_len];
	bool populated;
	int depth;
	following_t** following;
	following_t** blocking;
	int following_count;
	int blocking_count;
	int ref_count;
	int block_ref_count;
} following_t;

static int _following_compare(const void* a, const void* b)
{
	const char* ida = a;
	const following_t* const* followingb = b;
	return strcmp(ida, (*followingb)->id);
}

static bool _has_following_entry(const char* id, following_t** list, int count)
{
	return count ? bsearch(id, list, count, sizeof(following_t*), _following_compare) != NULL : false;
}

static bool _add_following_entry(following_t*** list, int* count, following_t* add)
{
	int index = tf_util_insert_index(add->id, *list, *count, sizeof(following_t*), _following_compare);
	if (index >= *count || strcmp(add->id, (*list)[index]->id) != 0)
	{
		*list = tf_resize_vec(*list, sizeof(**list) * (*count + 1));
		if (*count - index)
		{
			memmove(*list + index + 1, *list + index, sizeof(following_t*) * (*count - index));
		}
		(*list)[index] = add;
		(*count)++;
		return true;
	}
	return false;
}

static bool _remove_following_entry(following_t*** list, int* count, following_t* remove)
{
	int index = tf_util_insert_index(remove->id, *list, *count, sizeof(following_t*), _following_compare);
	if (index < *count && strcmp(remove->id, (*list)[index]->id) == 0)
	{
		if (*count - index > 1)
		{
			memmove(*list + index, *list + index + 1, sizeof(following_t*) * (*count - index - 1));
		}
		*list = tf_resize_vec(*list, sizeof(**list) * (*count - 1));
		(*count)--;
		return true;
	}
	return false;
}

typedef struct _block_node_t block_node_t;

typedef struct _block_node_t
{
	following_t* entry;
	block_node_t* parent;
} block_node_t;

static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blocks)
{
	for (const block_node_t* b = blocks; b; b = b->parent)
	{
		if (_has_following_entry(id, b->entry->blocking, b->entry->blocking_count))
		{
			return true;
		}
	}
	return false;
}

static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks, bool include_blocks)
{
	if (!include_blocks && _is_blocked_by_active_blocks(id, blocks))
	{
		return NULL;
	}
	int index = tf_util_insert_index(id, *following, *following_count, sizeof(following_t*), _following_compare);
	following_t* entry = NULL;
	if (index < *following_count && strcmp(id, (*following)[index]->id) == 0)
	{
		entry = (*following)[index];
	}
	else
	{
		*following = tf_resize_vec(*following, sizeof(*following) * (*following_count + 1));
		if (*following_count - index)
		{
			memmove(*following + index + 1, *following + index, sizeof(following_t*) * (*following_count - index));
		}
		entry = tf_malloc(sizeof(following_t));
		(*following)[index] = entry;
		(*following_count)++;
		memset(entry, 0, sizeof(*entry));
		entry->depth = INT_MAX;
		tf_string_set(entry->id, sizeof(entry->id), id);
	}
	return entry;
}

static void _populate_follows_and_blocks(
	sqlite3* db, sqlite3_stmt* statement, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks, bool include_blocks)
{
	if (sqlite3_bind_text(statement, 1, entry->id, -1, NULL) == SQLITE_OK)
	{
		while (sqlite3_step(statement) == SQLITE_ROW)
		{
			const char* contact = (const char*)sqlite3_column_text(statement, 0);
			if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
			{
				bool is_following = sqlite3_column_int(statement, 1) != 0;
				following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks);
				if (next)
				{
					if (is_following)
					{
						if (_add_following_entry(&entry->following, &entry->following_count, next))
						{
							next->ref_count++;
						}
					}
					else
					{
						if (_remove_following_entry(&entry->following, &entry->following_count, next))
						{
							next->ref_count--;
						}
					}
				}
			}
			if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
			{
				bool is_blocking = sqlite3_column_int(statement, 2) != 0;
				following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks);
				if (next)
				{
					if (is_blocking)
					{
						if (_add_following_entry(&entry->blocking, &entry->blocking_count, next))
						{
							next->block_ref_count++;
						}
					}
					else
					{
						if (_remove_following_entry(&entry->blocking, &entry->blocking_count, next))
						{
							next->block_ref_count--;
						}
					}
				}
			}
		}
	}
	sqlite3_reset(statement);
}

static void _get_following(sqlite3* db, sqlite3_stmt* statement, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth,
	block_node_t* active_blocks, bool include_blocks)
{
	int old_depth = entry->depth;
	entry->depth = tf_min(depth, entry->depth);
	if (depth < max_depth && depth < old_depth)
	{
		if (!_is_blocked_by_active_blocks(entry->id, active_blocks))
		{
			if (!entry->populated)
			{
				entry->populated = true;
				_populate_follows_and_blocks(db, statement, entry, following, following_count, active_blocks, include_blocks);
			}

			block_node_t blocks = { .entry = entry, .parent = active_blocks };
			for (int i = 0; i < entry->following_count; i++)
			{
				_get_following(db, statement, entry->following[i], following, following_count, depth + 1, max_depth, &blocks, include_blocks);
			}
		}
	}
}

static sqlite3_stmt* _make_following_statement(sqlite3* db)
{
	sqlite3_stmt* statement = NULL;

	if (sqlite3_prepare_v2(db,
			"SELECT content ->> '$.contact' AS contact, content ->> '$.following', content ->> '$.blocking' "
			"FROM messages "
			"WHERE author = ? AND content ->> '$.type' = 'contact' AND contact IS NOT NULL "
			"ORDER BY sequence",
			-1, &statement, NULL) != SQLITE_OK)
	{
		tf_printf("prepare failed: %s", sqlite3_errmsg(db));
	}
	return statement;
}

tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth, bool include_blocks)
{
	following_t** following = NULL;
	int following_count = 0;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = _make_following_statement(db);
	for (int i = 0; i < count; i++)
	{
		following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, include_blocks);
		_get_following(db, statement, entry, &following, &following_count, 0, depth, NULL, include_blocks);
		entry->ref_count++;
	}
	sqlite3_finalize(statement);
	tf_ssb_release_db_reader(ssb, db);

	int actual_following_count = 0;
	for (int i = 0; i < following_count; i++)
	{
		if (following[i]->ref_count > 0 || include_blocks)
		{
			actual_following_count++;
		}
	}

	tf_ssb_following_t* result = tf_malloc(sizeof(tf_ssb_following_t) * (actual_following_count + 1));
	int write_index = 0;
	for (int i = 0; i < following_count; i++)
	{
		if (following[i]->ref_count > 0 || include_blocks)
		{
			result[write_index] = (tf_ssb_following_t) {
				.following_count = following[i]->following_count,
				.blocking_count = following[i]->blocking_count,
				.followed_by_count = following[i]->ref_count,
				.blocked_by_count = following[i]->block_ref_count,
				.depth = following[i]->depth,
			};
			tf_string_set(result[write_index].id, sizeof(result[write_index].id), following[i]->id);
			write_index++;
		}
	}
	result[write_index] = (tf_ssb_following_t) { 0 };

	for (int i = 0; i < following_count; i++)
	{
		tf_free(following[i]->following);
		tf_free(following[i]->blocking);
		tf_free(following[i]);
	}
	tf_free(following);

	return result;
}

const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int count, int depth)
{
	following_t** following = NULL;
	int following_count = 0;

	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = _make_following_statement(db);
	for (int i = 0; i < count; i++)
	{
		following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, false);
		_get_following(db, statement, entry, &following, &following_count, 0, depth, NULL, false);
		entry->ref_count++;
	}
	sqlite3_finalize(statement);
	tf_ssb_release_db_reader(ssb, db);

	int actual_following_count = 0;
	for (int i = 0; i < following_count; i++)
	{
		if (following[i]->ref_count > 0)
		{
			actual_following_count++;
		}
	}

	char** result = tf_malloc(sizeof(char*) * (actual_following_count + 1) + k_id_base64_len * actual_following_count);
	char* result_ids = (char*)result + sizeof(char*) * (actual_following_count + 1);

	int write_index = 0;
	for (int i = 0; i < following_count; i++)
	{
		if (following[i]->ref_count > 0)
		{
			result[write_index] = result_ids + k_id_base64_len * write_index;
			tf_string_set(result[write_index], k_id_base64_len, following[i]->id);
			write_index++;
		}
	}
	result[actual_following_count] = NULL;

	for (int i = 0; i < following_count; i++)
	{
		tf_free(following[i]->following);
		tf_free(following[i]->blocking);
		tf_free(following[i]);
	}
	tf_free(following);

	return (const char**)result;
}

typedef struct _identities_t
{
	const char** ids;
	int count;
} identities_t;

static void _add_identity(const char* identity, void* user_data)
{
	identities_t* identities = user_data;
	char full_id[k_id_base64_len];
	snprintf(full_id, sizeof(full_id), "@%s", identity);
	identities->ids = tf_resize_vec(identities->ids, sizeof(const char*) * (identities->count + 1));
	identities->ids[identities->count++] = tf_strdup(full_id);
}

const char** tf_ssb_db_get_all_visible_identities(tf_ssb_t* ssb, int depth)
{
	identities_t identities = { 0 };
	tf_ssb_db_identity_visit_all(ssb, _add_identity, &identities);
	const char** following = tf_ssb_db_following_deep_ids(ssb, identities.ids, identities.count, depth);
	for (int i = 0; i < identities.count; i++)
	{
		tf_free((void*)identities.ids[i]);
	}
	tf_free(identities.ids);
	return following;
}

JSValue tf_ssb_db_get_message_by_id(tf_ssb_t* ssb, const char* id, bool is_keys)
{
	JSValue result = JS_UNDEFINED;
	JSContext* context = tf_ssb_get_context(ssb);
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT previous, author, id, sequence, timestamp, hash, json(content), signature, flags FROM messages WHERE id = ?", -1, &statement, NULL) ==
		SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				JSValue message = JS_UNDEFINED;
				JSValue formatted = tf_ssb_format_message(context, (const char*)sqlite3_column_text(statement, 0), (const char*)sqlite3_column_text(statement, 1),
					sqlite3_column_int(statement, 3), sqlite3_column_double(statement, 4), (const char*)sqlite3_column_text(statement, 5),
					(const char*)sqlite3_column_text(statement, 6), (const char*)sqlite3_column_text(statement, 7), sqlite3_column_int(statement, 8));
				if (is_keys)
				{
					message = JS_NewObject(context);
					JS_SetPropertyStr(context, message, "key", JS_NewString(context, (const char*)sqlite3_column_text(statement, 2)));
					JS_SetPropertyStr(context, message, "value", formatted);
					JS_SetPropertyStr(context, message, "timestamp", JS_NewString(context, (const char*)sqlite3_column_text(statement, 4)));
				}
				else
				{
					message = formatted;
				}
				result = message;
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

tf_ssb_db_stored_connection_t* tf_ssb_db_get_stored_connections(tf_ssb_t* ssb, int* out_count)
{
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	tf_ssb_db_stored_connection_t* result = NULL;
	int count = 0;

	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT host, port, key, last_attempt, last_success FROM connections ORDER BY host, port, key", -1, &statement, NULL) == SQLITE_OK)
	{
		while (sqlite3_step(statement) == SQLITE_ROW)
		{
			result = tf_resize_vec(result, sizeof(tf_ssb_db_stored_connection_t) * (count + 1));
			result[count] = (tf_ssb_db_stored_connection_t) {
				.port = sqlite3_column_int(statement, 1),
				.last_attempt = sqlite3_column_int64(statement, 3),
				.last_success = sqlite3_column_int64(statement, 4),
			};
			tf_string_set(result[count].address, sizeof(result[count].address), (const char*)sqlite3_column_text(statement, 0));
			tf_string_set(result[count].pubkey, sizeof(result[count].pubkey), (const char*)sqlite3_column_text(statement, 2));
			count++;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);

	*out_count = count;
	return result;
}

void tf_ssb_db_forget_stored_connection(tf_ssb_t* ssb, const char* address, int port, const char* pubkey)
{
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "DELETE FROM connections WHERE host = ? AND port = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, address, -1, NULL) != SQLITE_OK || sqlite3_bind_int(statement, 2, port) != SQLITE_OK ||
			sqlite3_bind_text(statement, 3, pubkey, -1, NULL) != SQLITE_OK || sqlite3_step(statement) != SQLITE_DONE)
		{
			tf_printf("Delete stored connection: %s.\n", sqlite3_errmsg(db));
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
}

bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char* out_password, size_t password_size)
{
	bool result = false;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				tf_string_set(out_password, password_size, (const char*)sqlite3_column_text(statement, 0));
				result = true;
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

bool tf_ssb_db_set_account_password(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password)
{
	bool result = false;
	static const int k_salt_length = 12;

	char buffer[16];
	size_t bytes = uv_random(loop, &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
	char output[7 + 22 + 1];
	char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output));
	char hash_output[7 + 22 + 31 + 1];
	char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output));

	JSValue user_entry = JS_NewObject(context);
	JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash));
	JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL);
	size_t user_length = 0;
	const char* user_string = JS_ToCStringLen(context, &user_length, user_json);

	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK)
		{
			result = sqlite3_step(statement) == SQLITE_DONE;
		}
		sqlite3_finalize(statement);
	}

	JS_FreeCString(context, user_string);
	JS_FreeValue(context, user_json);
	JS_FreeValue(context, user_entry);
	return result;
}

bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password)
{
	bool result = false;
	JSValue users_array = JS_UNDEFINED;

	bool registration_allowed = true;
	tf_ssb_db_get_global_setting_bool(db, "account_registration", &registration_allowed);
	if (registration_allowed)
	{
		sqlite3_stmt* statement = NULL;
		if (sqlite3_prepare_v2(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
			}
			sqlite3_finalize(statement);
		}
		if (JS_IsUndefined(users_array))
		{
			users_array = JS_NewArray(context);
		}
		int length = tf_util_get_length(context, users_array);
		JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));

		JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
		JS_FreeValue(context, users_array);
		size_t value_length = 0;
		const char* value = JS_ToCStringLen(context, &value_length, json);
		if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
			{
				result = sqlite3_step(statement) == SQLITE_DONE;
			}
			sqlite3_finalize(statement);
		}
		JS_FreeCString(context, value);
		JS_FreeValue(context, json);
	}

	result = result && tf_ssb_db_set_account_password(loop, db, context, name, password);
	return result;
}

const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* key)
{
	char* result = NULL;
	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				size_t length = sqlite3_column_bytes(statement, 0);
				result = tf_malloc(length + 1);
				memcpy(result, sqlite3_column_text(statement, 0), length);
				result[length] = '\0';
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_reader(ssb, db);
	return result;
}

bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
	bool result = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
		{
			result = sqlite3_step(statement) == SQLITE_DONE;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return result;
}

bool tf_ssb_db_remove_property(tf_ssb_t* ssb, const char* id, const char* key)
{
	bool result = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "DELETE FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
		{
			result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return result;
}

bool tf_ssb_db_remove_value_from_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
	bool result = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db,
			"UPDATE properties SET value = json_remove(properties.value, entry.fullkey) FROM json_each(properties.value) AS entry WHERE properties.id = ? AND properties.key = ? "
			"AND entry.value = ?",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
		{
			result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return result;
}

bool tf_ssb_db_add_value_to_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
	bool result = false;
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db,
			"INSERT INTO properties (id, key, value) VALUES (?1, ?2, json_array(?3)) ON CONFLICT DO UPDATE SET value = json_insert(properties.value, '$[#]', ?3) WHERE "
			"properties.id = ?1 AND properties.key = ?2 AND NOT EXISTS (SELECT 1 FROM json_each(properties.value) AS entry WHERE entry.value = ?3)",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
		{
			result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
	return result;
}

bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size)
{
	sqlite3_stmt* statement = NULL;
	bool found = false;
	if (sqlite3_prepare_v2(db, "SELECT value FROM properties WHERE id = ? AND key = 'id:' || ? || ':' || ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, package_owner, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_text(statement, 3, package_name, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
		{
			tf_string_set(out_identity, out_identity_size, (const char*)sqlite3_column_text(statement, 0));
			found = true;
		}
		sqlite3_finalize(statement);
	}
	return found;
}

typedef struct _resolve_index_t
{
	const char* host;
	const char* path;
	void (*callback)(const char* path, void* user_data);
	void* user_data;
} resolve_index_t;

static void _tf_ssb_db_resolve_index_work(tf_ssb_t* ssb, void* user_data)
{
	resolve_index_t* request = user_data;

	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.index_map') FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_step(statement) == SQLITE_ROW)
		{
			const char* index_map = (const char*)sqlite3_column_text(statement, 0);
			const char* start = index_map;
			while (start)
			{
				const char* end = strchr(start, '\n');
				const char* equals = strchr(start, '=');
				if (equals && strncasecmp(request->host, start, equals - start) == 0)
				{
					size_t value_length = end && equals < end ? (size_t)(end - (equals + 1)) : strlen(equals + 1);
					char* path = tf_malloc(value_length + 1);
					memcpy(path, equals + 1, value_length);
					path[value_length] = '\0';
					request->path = path;
					break;
				}
				start = end ? end + 1 : NULL;
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}

	if (!request->path)
	{
		if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.index') FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				request->path = tf_strdup((const char*)sqlite3_column_text(statement, 0));
			}
			sqlite3_finalize(statement);
		}
		else
		{
			tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
		}
	}
	tf_ssb_release_db_reader(ssb, db);

	if (!request->path)
	{
		request->path = tf_strdup(tf_util_get_default_global_setting_string("index"));
	}
}

static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
	resolve_index_t* request = user_data;
	request->callback(request->path, request->user_data);
	tf_free((void*)request->host);
	tf_free((void*)request->path);
	tf_free(request);
}

void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callback)(const char* path, void* user_data), void* user_data)
{
	resolve_index_t* request = tf_malloc(sizeof(resolve_index_t));
	*request = (resolve_index_t) {
		.host = tf_strdup(host),
		.callback = callback,
		.user_data = user_data,
	};
	tf_ssb_run_work(ssb, _tf_ssb_db_resolve_index_work, _tf_ssb_db_resolve_index_after_work, request);
}

static void _tf_ssb_db_set_flags(tf_ssb_t* ssb, const char* message_id, int flags)
{
	sqlite3* db = tf_ssb_acquire_db_writer(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(db, "UPDATE messages SET flags = ? WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_int(statement, 1, flags) == SQLITE_OK && sqlite3_bind_text(statement, 2, message_id, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) != SQLITE_DONE)
			{
				tf_printf("Setting flags of %s to %d failed: %s.\n", message_id, flags, sqlite3_errmsg(db));
			}
		}
		sqlite3_finalize(statement);
	}
	tf_ssb_release_db_writer(ssb, db);
}

bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int32_t debug_sequence, bool fix)
{
	JSContext* context = tf_ssb_get_context(ssb);
	bool verified = true;
	int32_t sequence = -1;
	if (tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0))
	{
		for (int32_t i = 1; i <= sequence; i++)
		{
			char message_id[k_id_base64_len];
			char previous[256];
			double timestamp;
			char* content = NULL;
			char hash[32];
			char signature[256];
			int flags = 0;
			if (tf_ssb_db_get_message_by_author_and_sequence(
					ssb, id, i, message_id, sizeof(message_id), previous, sizeof(previous), &timestamp, &content, hash, sizeof(hash), signature, sizeof(signature), &flags))
			{
				JSValue message = tf_ssb_format_message(context, previous, id, i, timestamp, hash, content, signature, flags);
				char calculated_id[k_id_base64_len];
				char extracted_signature[256];
				int calculated_flags = 0;
				if (!tf_ssb_verify_and_strip_signature(context, message, i == debug_sequence ? k_tf_ssb_verify_flag_debug : 0, calculated_id, sizeof(calculated_id),
						extracted_signature, sizeof(extracted_signature), &calculated_flags))
				{
					tf_printf("author=%s sequence=%d verify failed.\n", id, i);
					verified = false;
				}
				if (calculated_flags != flags)
				{
					tf_printf("author=%s sequence=%d flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
					if (fix)
					{
						_tf_ssb_db_set_flags(ssb, message_id, calculated_flags);
					}
					else
					{
						verified = false;
					}
				}
				if (strcmp(message_id, calculated_id))
				{
					tf_printf("author=%s sequence=%d id mismatch %s => %s.\n", id, i, message_id, calculated_id);
					verified = false;
				}
				JS_FreeValue(context, message);
				tf_free(content);

				if (!verified)
				{
					break;
				}
			}
			else
			{
				tf_printf("Unable to find message with sequence=%d for author=%s.", i, id);
				verified = false;
				break;
			}
		}
	}
	else
	{
		tf_printf("Unable to get latest message for author '%s'.\n", id);
		verified = false;
	}
	return verified;
}

bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, const char* permission)
{
	bool has_permission = false;
	sqlite3* reader = db ? db : tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	if (sqlite3_prepare_v2(reader,
			"SELECT COUNT(*) FROM properties, json_each(properties.value -> 'permissions' -> ?) AS permission WHERE properties.id = 'core' AND properties.key = 'settings' AND "
			"permission.value = ?",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, permission, -1, NULL) == SQLITE_OK &&
			sqlite3_step(statement) == SQLITE_ROW)
		{
			has_permission = sqlite3_column_int64(statement, 0) > 0;
		}
		sqlite3_finalize(statement);
	}
	if (reader != db)
	{
		tf_ssb_release_db_reader(ssb, reader);
	}
	return has_permission;
}

bool tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool* out_value)
{
	bool result = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
			{
				*out_value = sqlite3_column_int(statement, 0) != 0;
				result = true;
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	if (!result)
	{
		*out_value = tf_util_get_default_global_setting_bool(name);
	}
	return result;
}

bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t* out_value)
{
	bool result = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
			{
				*out_value = sqlite3_column_int64(statement, 0);
				result = true;
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	if (!result)
	{
		*out_value = tf_util_get_default_global_setting_int(name);
	}
	return result;
}

bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size)
{
	bool result = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
			{
				tf_string_set(out_value, size, (const char*)sqlite3_column_text(statement, 0));
				result = true;
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	if (!result)
	{
		tf_string_set(out_value, size, tf_util_get_default_global_setting_string(name));
	}
	return result;
}

bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value)
{
	tf_setting_kind_t kind = tf_util_get_global_setting_kind(name);
	if (kind == k_kind_unknown)
	{
		return false;
	}

	bool result = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db,
			"INSERT INTO properties (id, key, value) VALUES ('core', 'settings', json_object(?1, ?2)) ON CONFLICT DO UPDATE SET value = json_set(value, '$.' || ?1, ?2)", -1,
			&statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
		{
			bool bound = false;
			switch (kind)
			{
			case k_kind_bool:
				bound = sqlite3_bind_int(statement, 2, value && (strcmp(value, "true") == 0 || atoi(value))) == SQLITE_OK;
				break;
			case k_kind_int:
				bound = sqlite3_bind_int(statement, 2, atoi(value)) == SQLITE_OK;
				break;
			case k_kind_string:
				bound = sqlite3_bind_text(statement, 2, value, -1, NULL) == SQLITE_OK;
				break;
			case k_kind_unknown:
				break;
			}
			if (bound && sqlite3_step(statement) == SQLITE_DONE)
			{
				result = sqlite3_changes(db) != 0;
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	return result;
}

const char* tf_ssb_db_get_profile(sqlite3* db, const char* id)
{
	const char* result = NULL;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db,
			"SELECT json(json_group_object(key, value)) FROM (SELECT fields.key, RANK() OVER (PARTITION BY fields.key ORDER BY messages.sequence DESC) AS rank, fields.value FROM "
			"messages, json_each(messages.content) AS fields WHERE messages.author = ? AND messages.content ->> '$.type' = 'about' AND messages.content ->> '$.about' = "
			"messages.author AND NOT fields.key IN ('about', 'type')) WHERE rank = 1",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	return result;
}

const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id)
{
	const char* result = NULL;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db,
			"SELECT name FROM (SELECT messages.author, RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, "
			"messages.content ->> 'name' AS name FROM messages WHERE messages.author = ? "
			"AND messages.content ->> '$.type' = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) "
			"WHERE author_rank = 1",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_ROW)
			{
				result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
	return result;
}

static void _tf_ssb_db_invite_cleanup(sqlite3* db)
{
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "DELETE FROM invites WHERE use_count = 0 OR (expires > 0 AND expires < ?)", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_int64(statement, 1, (int64_t)time(NULL)) == SQLITE_OK)
		{
			if (sqlite3_step(statement) == SQLITE_DONE)
			{
				if (sqlite3_changes(db))
				{
					char buffer[2] = { 0 };
					size_t buffer_size = sizeof(buffer);
					bool verbose = uv_os_getenv("TF_SSB_VERBOSE", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
					if (verbose)
					{
						tf_printf("Cleaned up %d used/expired invites.\n", sqlite3_changes(db));
					}
				}
			}
			else
			{
				tf_printf("Invite cleanup failed: %s\n", sqlite3_errmsg(db));
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
	}
}

bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, int port, int use_count, int expires_seconds, char* out_invite, size_t size)
{
	if (use_count < -1 || use_count == 0)
	{
		return false;
	}

	uint8_t public_key[crypto_sign_ed25519_PUBLICKEYBYTES] = { 0 };
	uint8_t secret_key[crypto_sign_ed25519_SECRETKEYBYTES] = { 0 };
	uint8_t seed[crypto_sign_ed25519_SEEDBYTES] = { 0 };

	randombytes_buf(seed, sizeof(seed));
	crypto_sign_ed25519_seed_keypair(public_key, secret_key, seed);

	char public[k_id_base64_len];
	tf_ssb_id_bin_to_str(public, sizeof(public), public_key);

	char seed_b64[64];
	tf_base64_encode(seed, sizeof(seed), seed_b64, sizeof(seed_b64));

	bool inserted = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "INSERT INTO invites (invite_public_key, account, use_count, expires) VALUES (?, ?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, public, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, id, -1, NULL) == SQLITE_OK &&
			sqlite3_bind_int(statement, 3, use_count) == SQLITE_OK &&
			sqlite3_bind_int64(statement, 4, (expires_seconds > 0 ? (int64_t)time(NULL) : 0) + expires_seconds) == SQLITE_OK)
		{
			inserted = sqlite3_step(statement) == SQLITE_DONE;
		}
		sqlite3_finalize(statement);
	}

	_tf_ssb_db_invite_cleanup(db);

	snprintf(out_invite, size, "%s:%d:%s~%s", host, port, id, seed_b64);
	return inserted;
}

bool tf_ssb_db_use_invite(sqlite3* db, const char* id)
{
	bool used = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "UPDATE invites SET use_count = use_count - 1 WHERE invite_public_key = ? AND (expires < 0 OR expires >= ?) AND (use_count > 0 OR use_count = -1)",
			-1, &statement, NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, (int64_t)time(NULL)) == SQLITE_OK)
		{
			used = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) > 0;
		}
		sqlite3_finalize(statement);
	}

	_tf_ssb_db_invite_cleanup(db);

	return used;
}

bool tf_ssb_db_has_invite(sqlite3* db, const char* id)
{
	bool has = false;
	sqlite3_stmt* statement;
	if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM invites WHERE invite_public_key = ? AND (expires < 0 OR expires >= ?) AND (use_count > 0 OR use_count = -1)", -1, &statement,
			NULL) == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, (int64_t)time(NULL)) == SQLITE_OK)
		{
			has = sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int(statement, 0) > 0;
		}
		sqlite3_finalize(statement);
	}
	return has;
}

static void _tf_ssb_db_get_identity_info_visit(const char* identity, void* user_data)
{
	tf_ssb_identity_info_t* info = user_data;
	info->identity = tf_resize_vec(info->identity, (info->count + 1) * sizeof(char*));
	info->name = tf_resize_vec(info->name, (info->count + 1) * sizeof(char*));
	char buffer[k_id_base64_len];
	snprintf(buffer, sizeof(buffer), "@%s", identity);
	info->identity[info->count] = tf_strdup(buffer);
	info->name[info->count] = NULL;
	info->count++;
}

tf_ssb_identity_info_t* tf_ssb_db_get_identity_info(tf_ssb_t* ssb, const char* user, const char* package_owner, const char* package_name)
{
	tf_ssb_identity_info_t* info = tf_malloc(sizeof(tf_ssb_identity_info_t));
	*info = (tf_ssb_identity_info_t) { 0 };

	char id[k_id_base64_len] = "";
	if (tf_ssb_db_user_has_permission(ssb, NULL, user, "administration"))
	{
		if (tf_ssb_whoami(ssb, id, sizeof(id)))
		{
			_tf_ssb_db_get_identity_info_visit(*id == '@' ? id + 1 : id, info);
		}
	}
	tf_ssb_db_identity_visit(ssb, user, _tf_ssb_db_get_identity_info_visit, info);

	sqlite3* db = tf_ssb_acquire_db_reader(ssb);
	sqlite3_stmt* statement = NULL;
	int result = sqlite3_prepare_v2(db,
		"SELECT author, name FROM ( "
		"	SELECT "
		"		messages.author, "
		"		RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, "
		"		messages.content ->> 'name' AS name "
		"	FROM messages "
		"	JOIN identities ON messages.author = ('@' || identities.public_key) "
		"	WHERE "
		"		(identities.user = ? OR identities.public_key = ?) AND "
		"		messages.content ->> '$.type' = 'about' AND "
		"		content ->> 'about' = messages.author AND name IS NOT NULL) "
		"WHERE author_rank = 1 ",
		-1, &statement, NULL);
	if (result == SQLITE_OK)
	{
		if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, *id == '@' ? id + 1 : id, -1, NULL) == SQLITE_OK)
		{
			int r = SQLITE_OK;
			while ((r = sqlite3_step(statement)) == SQLITE_ROW)
			{
				const char* identity = (const char*)sqlite3_column_text(statement, 0);
				const char* name = (const char*)sqlite3_column_text(statement, 1);
				for (int i = 0; i < info->count; i++)
				{
					if (!info->name[i] && strcmp(info->identity[i], identity) == 0)
					{
						info->name[i] = tf_strdup(name);
						break;
					}
				}
			}
		}
		sqlite3_finalize(statement);
	}
	else
	{
		tf_printf("prepare failed: %s.\n", sqlite3_errmsg(db));
	}

	tf_ssb_db_identity_get_active(db, user, package_owner, package_name, info->active_identity, sizeof(info->active_identity));
	if (!*info->active_identity && info->count)
	{
		tf_string_set(info->active_identity, sizeof(info->active_identity), info->identity[0]);
	}
	tf_ssb_release_db_reader(ssb, db);

	size_t size = sizeof(tf_ssb_identity_info_t) + sizeof(char*) * info->count * 2;
	for (int i = 0; i < info->count; i++)
	{
		size += strlen(info->identity[i]) + 1;
		size += info->name[i] ? strlen(info->name[i]) + 1 : 0;
	}
	tf_ssb_identity_info_t* copy = tf_malloc(size);
	*copy = *info;

	copy->identity = (const char**)(copy + 1);
	copy->name = (const char**)(copy + 1) + copy->count;

	char* p = (char*)((const char**)(copy + 1) + copy->count * 2);

	for (int i = 0; i < info->count; i++)
	{
		size_t length = strlen(info->identity[i]);
		memcpy(p, info->identity[i], length + 1);
		copy->identity[i] = p;
		p += length + 1;
		tf_free((void*)info->identity[i]);

		if (info->name[i])
		{
			length = strlen(info->name[i]);
			memcpy(p, info->name[i], length + 1);
			copy->name[i] = p;
			p += length + 1;
			tf_free((void*)info->name[i]);
		}
		else
		{
			copy->name[i] = NULL;
		}
	}
	tf_free(info->name);
	tf_free(info->identity);
	tf_free(info);
	return copy;
}
