// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.

namespace ircd::m
{
	static json::object make_hashes(const mutable_buffer &out, const sha256::buf &hash);
}

/// The maximum size of an event we will create. This may also be used in
/// some contexts for what we will accept, but the protocol limit and hard
/// worst-case buffer size is still event::MAX_SIZE.
decltype(ircd::m::event::max_size)
ircd::m::event::max_size
{
	{ "name",     "ircd.m.event.max_size" },
	{ "default",   65507L                 },
};

[[gnu::tls_model("global-dynamic")]]
decltype(ircd::m::event::buf)
thread_local
ircd::m::event::buf;

bool
ircd::m::check_id(const event &event)
noexcept
{
	if(!event.event_id)
		return false;

	const string_view &version
	{
		event.event_id.version()
	};

	return check_id(event, version);
}

bool
ircd::m::check_id(const event &event,
                  const string_view &room_version)
noexcept try
{
	assert(event.event_id);
	const auto &version
	{
		room_version?: event.event_id.version()
	};

	char buf[64];
	const event::id &check_id
	{
		version == "1" || version == "2"?
			event::id{json::get<"event_id"_>(event)}:

		version == "3"?
			event::id{event::id::v3{buf, event}}:

		event::id{event::id::v4{buf, event}}
	};

	return event.event_id == check_id;
}
catch(const std::exception &e)
{
	log::error
	{
		"m::check_id() :%s", e.what()
	};

	return false;
}
catch(...)
{
	assert(0);
	return false;
}

ircd::m::id::event
ircd::m::make_id(const event &event,
                 const string_view &version,
                 id::event::buf &buf)
{
	if(version == "1" || version == "2")
	{
		const crh::sha256::buf hash{event};
		return make_id(event, version, buf, hash);
	}

	if(version == "3")
		return event::id::v3
		{
			buf, event
		};

	return event::id::v4
	{
		buf, event
	};
}

ircd::m::id::event
ircd::m::make_id(const event &event,
                 const string_view &version,
                 id::event::buf &buf,
                 const const_buffer &hash)
{
	char readable[b64::encode_size(sha256::digest_size)];

	if(version == "1" || version == "2")
	{
		const id::event ret
		{
			buf, b64::encode_unpadded(readable, hash, b64::urlsafe), at<"origin"_>(event)
		};

		buf.assigned(ret);
		return ret;
	}
	else if(version == "3")
	{
		const id::event ret
		{
			buf, b64::encode_unpadded(readable, hash), string_view{}
		};

		buf.assigned(ret);
		return ret;
	}

	const id::event ret
	{
		buf, b64::encode_unpadded(readable, hash, b64::urlsafe), string_view{}
	};

	buf.assigned(ret);
	return ret;
}

ircd::json::object
ircd::m::hashes(const mutable_buffer &out,
                const event &event)
{
	const sha256::buf hash_
	{
		hash(event)
	};

	return make_hashes(out, hash_);
}

ircd::json::object
ircd::m::event::hashes(const mutable_buffer &out,
                       json::iov &event,
                       const string_view &content)
{
	const sha256::buf hash_
	{
		hash(event, content)
	};

	return make_hashes(out, hash_);
}

ircd::json::object
ircd::m::make_hashes(const mutable_buffer &out,
                     const sha256::buf &hash)
{
	static const auto b64bufsz
	{
		b64::encode_size(sizeof(hash))
	};

	char hashb64buf[b64bufsz];
	const json::members hashes
	{
		{ "sha256", b64::encode_unpadded(hashb64buf, hash) }
	};

	return json::stringify(mutable_buffer{out}, hashes);
}

ircd::sha256::buf
ircd::m::event::hash(const json::object &event_)
{
	const json::object preimage
	{
		event::preimage(buf[3], event_)
	};

	return sha256
	{
		preimage
	};
}

ircd::sha256::buf
ircd::m::event::hash(json::iov &event,
                     const string_view &content)
{
	const json::iov::push _content
	{
		event, { "content", content }
	};

	return m::hash(m::event{event});
}

ircd::sha256::buf
ircd::m::hash(const event &event)
{
	if(event.source)
		return event::hash(event.source);

	m::event event_{event};
	json::get<"signatures"_>(event_) = {};
	json::get<"hashes"_>(event_) = {};
	const string_view preimage
	{
		stringify(event::buf[3], event_)
	};

	return sha256{preimage};
}

bool
ircd::m::verify_hash(const event &event)
{
	const sha256::buf hash
	{
		m::hash(event)
	};

	return verify_hash(event, hash);
}

bool
ircd::m::verify_hash(const event &event,
                     const sha256::buf &actual)
try
{
	const json::object &object
	{
		at<"hashes"_>(event)
	};

	const json::string &hash
	{
		object.at("sha256")
	};

	char buf[32];
	const auto claim
	{
		b64::decode(buf, hash)
	};

	static_assert(sizeof(buf) == sizeof(actual));
	if(unlikely(ircd::size(claim) != sizeof(actual)))
		return false;

	if(memcmp(buf, ircd::data(actual), sizeof(buf)) != 0)
		return false;

	return true;
}
catch(const json::not_found &)
{
	return false;
}

ircd::json::object
ircd::m::event::signatures(const mutable_buffer &out,
                           json::iov &event,
                           const json::iov &content)
{
	const string_view &origin
	{
		event.at("origin")
	};

	const ed25519::sig sig
	{
		sign(event, content)
	};

	const string_view public_key_id
	{
		m::public_key_id(m::my(origin))
	};

	char sigb64buf[b64::encode_size(sizeof(sig))];
	const json::members sigb64
	{
		{ public_key_id, b64::encode_unpadded(sigb64buf, sig) }
	};

	const json::members sigs
	{
		{ origin, sigb64 }
    };

	return json::stringify(mutable_buffer{out}, sigs);
}

ircd::m::event
ircd::m::signatures(const mutable_buffer &out,
                    const m::event &event)
{
	const string_view &origin
	{
		at<"origin"_>(event)
	};

	return signatures(out, event, origin);
}

ircd::m::event
ircd::m::signatures(const mutable_buffer &out,
                    const m::event &event,
                    const string_view &origin)
{
	const auto &secret_key
	{
		m::secret_key(my(origin))
	};

	const string_view public_key_id
	{
		m::public_key_id(my(origin))
	};

	return signatures(out, event, origin, secret_key, public_key_id);
}

ircd::m::event
ircd::m::signatures(const mutable_buffer &out_,
                    const m::event &event_,
                    const string_view &origin,
                    const ed25519::sk &secret_key,
                    const string_view &public_key_id)
{
	m::event event
	{
		essential(event_, event::buf[3])
	};

	const string_view &preimage
	{
		stringify(event::buf[2], event)
	};

	const ed25519::sig my_sig
	{
		event::sign(preimage, secret_key)
	};

	static const auto sigb64bufsz
	{
		b64::encode_size(sizeof(my_sig))
	};

	char sigb64buf[sigb64bufsz];
	const json::member my_sig_member
	{
		origin, json::members
		{
			{ public_key_id, b64::encode_unpadded(sigb64buf, my_sig) }
		}
	};

	static const size_t SIG_MAX{64};
	thread_local std::array<json::member, SIG_MAX> sigs;

	size_t i(0);
	sigs.at(i++) = my_sig_member;
	for(const auto &[host, sig] : json::get<"signatures"_>(event_))
		if(json::string(host) != origin)
			sigs.at(i++) = { host, sig };

	event = event_;
	mutable_buffer out{out_};
	json::get<"signatures"_>(event) = json::stringify(out, sigs.data(), sigs.data() + i);
	return event;
}

ircd::ed25519::sig
ircd::m::event::sign(json::iov &event,
                     const json::iov &contents)
{
	const string_view &origin
	{
		event.at("origin")
	};

	const auto &secret_key
	{
		m::secret_key(m::my(origin))
	};

	return sign(event, contents, secret_key);
}

ircd::ed25519::sig
ircd::m::event::sign(json::iov &event,
                     const json::iov &contents,
                     const ed25519::sk &sk)
{
	ed25519::sig sig;
	essential(event, contents, [&sk, &sig]
	(json::iov &event)
	{
		sig = m::sign(m::event{event}, sk);
	});

	return sig;
}

ircd::ed25519::sig
ircd::m::sign(const event &event)
{
	const string_view &origin
	{
		at<"origin"_>(event)
	};

	return sign(event, origin);
}

ircd::ed25519::sig
ircd::m::sign(const event &event,
              const string_view &origin)
{
	const auto &secret_key
	{
		m::secret_key(my(origin))
	};

	return sign(event, secret_key);
}

ircd::ed25519::sig
ircd::m::sign(const event &event,
              const ed25519::sk &sk)
{
	const string_view preimage
	{
		stringify(event::buf[3], event)
	};

	return event::sign(preimage, sk);
}

ircd::ed25519::sig
ircd::m::event::sign(const json::object &event)
{
	const json::string &origin
	{
		event.at("origin")
	};

	const auto &secret_key
	{
		m::secret_key(m::my(origin))
	};

	return sign(event, secret_key);
}

ircd::ed25519::sig
ircd::m::event::sign(const json::object &event,
                     const ed25519::sk &sk)
{
	//TODO: skip rewrite
	const string_view preimage
	{
		stringify(buf[3], event)
	};

	return sign(preimage, sk);
}

ircd::ed25519::sig
ircd::m::event::sign(const string_view &event)
{
	const json::string &origin
	{
		json::object(event).at("origin")
	};

	const auto &secret_key
	{
		m::secret_key(m::my(origin))
	};

	return sign(event, secret_key);
}

ircd::ed25519::sig
ircd::m::event::sign(const string_view &event,
                     const ed25519::sk &sk)
{
	const ed25519::sig sig
	{
		sk.sign(event)
	};

	return sig;
}
bool
ircd::m::verify(const event &event)
{
	const string_view &origin
	{
		at<"origin"_>(event)
	};

	return verify(event, origin);
}

bool
ircd::m::verify(const event &event,
                const string_view &origin)
{
	const json::object &signatures
	{
		at<"signatures"_>(event)
	};

	const json::object &origin_sigs
	{
		signatures.at(origin)
	};

	for(const auto &[keyid, sig] : origin_sigs)
		if(verify(event, origin, json::string(keyid)))
			return true;

	return false;
}

bool
ircd::m::verify(const event &event,
                const string_view &origin,
                const string_view &keyid)
try
{
	const m::node::keys node_keys
	{
		origin
	};

	bool ret{false};
	node_keys.get(keyid, [&ret, &event, &origin, &keyid]
	(const ed25519::pk &pk)
	{
		ret = verify(event, pk, origin, keyid);
	});

	return ret;
}
catch(const ctx::interrupted &e)
{
	log::error
	{
		log, "Failed to verify %s because key %s for %s :%s",
		string_view{event.event_id},
		keyid,
		origin,
		e.what(),
	};

	throw;
}
catch(const std::exception &e)
{
	throw m::error
	{
		http::UNAUTHORIZED, "M_UNVERIFIABLE_SIGNATURE",
		"%s key %s :%s",
		origin,
		keyid,
		e.what(),
	};
}

bool
ircd::m::verify(const event &event,
                const ed25519::pk &pk,
                const string_view &origin,
                const string_view &keyid)
{
	const json::object &signatures
	{
		at<"signatures"_>(event)
	};

	const json::object &origin_sigs
	{
		signatures.at(origin)
	};

	const ed25519::sig sig
	{
		[&origin_sigs, &keyid](auto&& buf)
		{
			b64::decode(buf, json::string(origin_sigs.at(keyid)));
		}
	};

	return verify(event, pk, sig);
}

bool
ircd::m::verify(const event &event_,
                const ed25519::pk &pk,
                const ed25519::sig &sig)
{
	m::event event
	{
		essential(event_, event::buf[3])
	};

	const json::object &preimage
	{
		stringify(event::buf[2], event)
	};

	return pk.verify(preimage, sig);
}

bool
ircd::m::event::verify(const json::object &event,
                       const ed25519::pk &pk,
                       const ed25519::sig &sig,
                       const bool &canonical)
{
	const auto preimage
	{
		canonical?
			string_view{event}:
			stringify(buf[3], event)
	};

	return pk.verify(preimage, sig);
}

void
ircd::m::event::essential(json::iov &event,
                          const json::iov &contents,
                          const event::closure_iov_mutable &closure)
try
{
	const auto &type
	{
		event.at("type")
	};

	if(type == "m.room.aliases")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "aliases", contents.at("aliases") }
			}
		}};

		closure(event);
	}
	else if(type == "m.room.create")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "creator", contents.at("creator") }
			}
		}};

		closure(event);
	}
	else if(type == "m.room.history_visibility")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "history_visibility", contents.at("history_visibility") }
			}
		}};

		closure(event);
	}
	else if(type == "m.room.join_rules")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "join_rule", contents.at("join_rule") }
			}
		}};

		closure(event);
	}
	else if(type == "m.room.member")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "membership", contents.at("membership") }
			}
		}};

		closure(event);
	}
	else if(type == "m.room.power_levels")
	{
		const json::iov::push _content{event,
		{
			"content", json::members
			{
				{ "ban", contents.at("ban")                        },
				{ "events", contents.at("events")                  },
				{ "events_default", contents.at("events_default")  },
				{ "kick", contents.at("kick")                      },
				{ "redact", contents.at("redact")                  },
				{ "state_default", contents.at("state_default")    },
				{ "users", contents.at("users")                    },
				{ "users_default", contents.at("users_default")    },
			}
		}};

		closure(event);
	}
	else if(type == "m.room.redaction")
	{
		// This simply finds the redacts key and swaps it with jsundefined for
		// the scope's duration. The redacts key will still be present and
		// visible in the json::iov which is incorrect if directly serialized.
		// However, this iov is turned into a json::tuple (m::event) which ends
		// up being serialized for signing. That serialization is where the
		// jsundefined redacts value is ignored.
		auto &redacts{event.at("redacts")};
		json::value temp(std::move(redacts));
		redacts = json::value{};
		const unwind _{[&redacts, &temp]
		{
			redacts = std::move(temp);
		}};

		const json::iov::push _content
		{
			event, { "content", "{}" }
		};

		closure(event);
	}
	else
	{
		const json::iov::push _content
		{
			event, { "content", "{}" }
		};

		closure(event);
	}
}
catch(const json::not_found &e)
{
	log::derror
	{
		log, "Error while isolating essential keys (redaction algorithm) :%s",
		e.what(),
	};

	throw;
}

ircd::m::event
ircd::m::essential(m::event event,
                   const mutable_buffer &contentbuf,
                   const bool &sigs)
try
{
	const auto &type
	{
		json::get<"type"_>(event)
	};

	json::object &content
	{
		json::get<"content"_>(event)
	};

	mutable_buffer essential
	{
		contentbuf
	};

	if(type == "m.room.aliases")
	{
		if(content.has("aliases"))
			content = json::stringify(essential, json::members
			{
				{ "aliases", content.at("aliases") }
			});
	}
	else if(type == "m.room.create")
	{
		if(content.has("creator"))
			content = json::stringify(essential, json::members
			{
				{ "creator", content.at("creator") }
			});
	}
	else if(type == "m.room.history_visibility")
	{
		if(content.has("history_visibility"))
			content = json::stringify(essential, json::members
			{
				{ "history_visibility", content.at("history_visibility") }
			});
	}
	else if(type == "m.room.join_rules")
	{
		if(content.has("join_rule"))
			content = json::stringify(essential, json::members
			{
				{ "join_rule", content.at("join_rule") }
			});
	}
	else if(type == "m.room.member")
	{
		if(content.has("membership"))
			content = json::stringify(essential, json::members
			{
				{ "membership", content.at("membership") }
			});
	}
	else if(type == "m.room.power_levels")
	{
		json::stack out{essential};
		json::stack::object top{out};

		if(content.has("ban"))
			json::stack::member
			{
				top, "ban", content.at("ban")
			};

		if(content.has("events"))
			json::stack::member
			{
				top, "events", content.at("events")
			};

		if(content.has("events_default"))
			json::stack::member
			{
				top, "events_default", content.at("events_default")
			};

		if(content.has("kick"))
			json::stack::member
			{
				top, "kick", content.at("kick")
			};

		if(content.has("redact"))
			json::stack::member
			{
				top, "redact", content.at("redact")
			};

		if(content.has("state_default"))
			json::stack::member
			{
				top, "state_default", content.at("state_default")
			};

		if(content.has("users"))
			json::stack::member
			{
				top, "users", content.at("users")
			};

		if(content.has("users_default"))
			json::stack::member
			{
				top, "users_default", content.at("users_default")
			};

		top.~object();
		content = out.completed();
	}
	else if(type == "m.room.redaction")
	{
		json::get<"redacts"_>(event) = string_view{};
		content = "{}"_sv;
	}
	else
	{
		content = "{}"_sv;
	}

	if(likely(!sigs))
		json::get<"signatures"_>(event) = {};

	return event;
}
catch(const json::not_found &e)
{
	log::derror
	{
		log, "Error while isolating essential keys (redaction algorithm) :%s",
		e.what(),
	};

	throw;
}

ircd::json::object
ircd::m::event::preimage(const mutable_buffer &buf_,
                         const json::object &event)
try
{
	static const size_t iov_max{json::iov::max_size};
	thread_local std::array<json::object::member, iov_max> member;

	size_t i(0);
	for(const auto &m : event)
	{
		if(m.first == "signatures" ||
		   m.first == "hashes" ||
		   m.first == "unsigned" ||
		   m.first == "age_ts" ||
		   m.first == "outlier" ||
		   m.first == "destinations")
			continue;

		member.at(i++) = m;
	}

	mutable_buffer buf{buf_};
	const string_view ret
	{
		json::stringify(buf, member.data(), member.data() + i)
	};

	return ret;
}
catch(const std::out_of_range &e)
{
	throw m::BAD_JSON
	{
		"Object has more than %zu member properties.",
		json::iov::max_size
	};
}

bool
ircd::m::before(const event &a,
                const event &b)
{
	const event::prev prev{b};
	return prev.prev_events_has(a.event_id);
}

size_t
ircd::m::degree(const event &event)
{
	return degree(event::prev{event});
}

bool
ircd::m::operator>=(const event &a, const event &b)
{
	const int room_id_cmp
	{
		cmp(json::get<"room_id"_>(a), json::get<"room_id"_>(b))
	};

	const auto depth_gte
	{
		json::get<"depth"_>(a) >= json::get<"depth"_>(b)
	};

	return room_id_cmp > 0 || (room_id_cmp == 0 && depth_gte);
}

bool
ircd::m::operator<=(const event &a, const event &b)
{
	const int room_id_cmp
	{
		cmp(json::get<"room_id"_>(a), json::get<"room_id"_>(b))
	};

	const auto depth_lte
	{
		json::get<"depth"_>(a) <= json::get<"depth"_>(b)
	};

	return room_id_cmp < 0 || (room_id_cmp == 0 && depth_lte);
}

bool
ircd::m::operator>(const event &a, const event &b)
{
	const int room_id_cmp
	{
		cmp(json::get<"room_id"_>(a), json::get<"room_id"_>(b))
	};

	const auto depth_gt
	{
		json::get<"depth"_>(a) > json::get<"depth"_>(b)
	};

	return room_id_cmp > 0 || (room_id_cmp == 0 && depth_gt);
}

bool
ircd::m::operator<(const event &a,
                   const event &b)
{
	const int room_id_cmp
	{
		cmp(json::get<"room_id"_>(a), json::get<"room_id"_>(b))
	};

	const auto depth_lt
	{
		json::get<"depth"_>(a) < json::get<"depth"_>(b)
	};

	return room_id_cmp < 0 || (room_id_cmp == 0 && depth_lt);
}

bool
ircd::m::operator==(const event &a, const event &b)
{
	//assert(json::get<"room_id"_>(a) == json::get<"room_id"_>(b));
	return a.event_id == b.event_id;
}

size_t
ircd::m::exists_count(const vector_view<const id::event> &event_ids)
{
	const auto mask
	{
		exists(event_ids)
	};

	const auto ret
	{
		popcount(mask)
	};

	assert(size_t(ret) <= event_ids.size());
	return ret;
}

uint64_t
ircd::m::exists(const vector_view<const id::event> &event_ids)
{
	const vector_view<const string_view> key
	{
		static_cast<const string_view *>(event_ids.data()), event_ids.size()
	};

	auto &column
	{
		dbs::event_idx
	};

	return db::has(column, key);
}

bool
ircd::m::bad(const id::event &event_id)
{
	bool ret {false};
	index(std::nothrow, event_id, [&ret]
	(const event::idx &event_idx) noexcept
	{
		ret = event_idx == 0;
	});

	return ret;
}

bool
ircd::m::good(const id::event &event_id)
{
	return bool(event_id) && index(std::nothrow, event_id) != 0;
}

bool
ircd::m::exists(const id::event &event_id,
                const bool &good)
{
	return good?
		m::good(event_id):
		m::exists(event_id);
}

bool
ircd::m::exists(const id::event &event_id)
{
	auto &column
	{
		dbs::event_idx
	};

	return bool(event_id) && has(column, event_id);
}

bool
ircd::m::my(const event &event)
{
	const auto &origin(json::get<"origin"_>(event));
	const auto &sender(json::get<"sender"_>(event));
	const auto &eid(event.event_id);
	return
		origin?
			my_host(origin):
		sender?
			my_host(user::id(sender).host()):
		eid?
			my(event::id(eid)):
		false;
}

bool
ircd::m::my(const id::event &event_id)
{
	return event_id.version() == "1"?
		self::host(event_id.host()):
		event::my(index(event_id));
}

bool
ircd::m::event::my(const idx &event_idx)
{
	return m::query(std::nothrow, event_idx, "origin", []
	(const string_view &origin)
	{
		return m::my_host(origin);
	});
}

//
// event::event
//

ircd::m::event::event(const json::members &members)
:super_type
{
	members
}
,event_id
{
	defined(json::get<"event_id"_>(*this))?
		id{json::get<"event_id"_>(*this)}:
		id{},
}
{
}

ircd::m::event::event(const json::iov &members)
:event
{
	members,
	members.has("event_id")?
		id{members.at("event_id")}:
		id{}
}
{
}

ircd::m::event::event(const json::iov &members,
                      const id &id)
:super_type
{
	members
}
,event_id
{
	id
}
{
}

ircd::m::event::event(const json::object &source)
:super_type
{
	source
}
,event_id
{
	defined(json::get<"event_id"_>(*this))?
		id{json::get<"event_id"_>(*this)}:
		id{},
}
{
}

ircd::m::event::event(const json::object &source,
                      const keys &keys)
:super_type
{
	source, keys
}
,event_id
{
	defined(json::get<"event_id"_>(*this))?
		id{json::get<"event_id"_>(*this)}:
		id{},
}
{
}

ircd::m::event::event(id::buf &buf,
                      const json::object &source,
                      const string_view &version)
:event
{
	source,
	version == "1"?
		id{json::string(source.get("event_id"))}:
	version == "2"?
		id{json::string(source.get("event_id"))}:
	version == "3"?
		id{id::v3{buf, source}}:
	version == "4"?
		id{id::v4{buf, source}}:
	source.has("event_id")?
		id{json::string(source.at("event_id"))}:
		id{id::v4{buf, source}},
}
{
}

ircd::m::event::event(const json::object &source,
                      const id &event_id)
try
:super_type
{
	source
}
,event_id
{
	event_id?
		event_id:
	defined(json::get<"event_id"_>(*this))?
		id{json::get<"event_id"_>(*this)}:
		id{},
}
{
}
catch(const json::parse_error &e)
{
	log::error
	{
		log, "Event %s from JSON source (%zu bytes) :%s",
		event_id?
			string_view{event_id}:
			"<event_id in source>"_sv,
		string_view{source}.size(),
		e.what(),
	};
}

ircd::m::event::event(const json::object &source,
                      const id &event_id,
                      const keys &keys)
try
:super_type
{
	source, keys
}
,event_id
{
	event_id?
		event_id:
	defined(json::get<"event_id"_>(*this))?
		id{json::get<"event_id"_>(*this)}:
		id{},
}
{
}
catch(const json::parse_error &e)
{
	log::error
	{
		log, "Event %s from JSON source (%zu bytes) keys:%zu :%s",
		string_view{event_id},
		string_view{source}.size(),
		keys.count(),
		e.what(),
	};
}
