// 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::vm
{
	extern m::hookfn<eval &> conform_check_event_id;
	extern m::hookfn<eval &> conform_check_origin;
	extern m::hookfn<eval &> conform_check_size;
	extern m::hookfn<eval &> conform_report;
}

/// Check if event_id is sufficient for the room version.
decltype(ircd::m::vm::conform_check_event_id)
ircd::m::vm::conform_check_event_id
{
	{
		{ "_site", "vm.conform" }
	},
	[](const m::event &event, eval &eval)
	{
		// Don't care about EDU's on this hook
		if(!event.event_id)
			return;

		// Conditions for when we don't care if the event_id conforms. This
		// hook only cares if the event_id is sufficient for the version, and
		// we don't care about the early matrix versions with mxids here.
		const bool unaffected
		{
			!eval.room_version
			|| eval.room_version == "0"
			|| eval.room_version == "1"
			|| eval.room_version == "2"
		};

		if(eval.room_version == "3")
			if(!event::id::v3::is(event.event_id))
				throw error
				{
					fault::INVALID, "Event ID %s is not sufficient for version 3 room.",
					string_view{event.event_id}
				};

		// note: we check v4 format for all other room versions, including "4"
		if(!unaffected && eval.room_version != "3")
			if(!event::id::v4::is(event.event_id))
				throw error
				{
					fault::INVALID, "Event ID %s in a version %s room is not a version 4 Event ID.",
					string_view{event.event_id},
					eval.room_version,
				};
	}
};

/// Check if an eval with a copts structure (indicating this server is
/// creating the event) has an origin set to !my_host().
decltype(ircd::m::vm::conform_check_origin)
ircd::m::vm::conform_check_origin
{
	{
		{ "_site", "vm.conform" }
	},
	[](const m::event &event, eval &eval)
	{
		if(eval.opts && !eval.opts->conforming)
			return;

		if(unlikely(eval.copts && !my_host(at<"origin"_>(event))))
			throw error
			{
				fault::INVALID, "Issuing event for origin: %s", at<"origin"_>(event)
			};
	}
};

/// Check if an event originating from this server exceeds maximum size.
decltype(ircd::m::vm::conform_check_size)
ircd::m::vm::conform_check_size
{
	{
		{ "_site",  "vm.conform"  },
	},
	[](const m::event &event, eval &eval)
	{
		const size_t &event_size
		{
			serialized(event)
		};

		if(event_size > size_t(event::max_size))
			throw m::BAD_JSON
			{
				"Event is %zu bytes which is larger than the maximum %zu bytes",
				event_size,
				size_t(event::max_size)
			};
	}
};

/// Check if an event originating from this server exceeds maximum size.
decltype(ircd::m::vm::conform_report)
ircd::m::vm::conform_report
{
	{
		{ "_site",  "vm.conform"  }
	},
	[](const m::event &event, eval &eval)
	{
		assert(eval.opts);
		auto &opts(*eval.opts);

		// When opts.conformed is set the report is already generated
		if(opts.conformed)
		{
			eval.report = opts.report;
			return;
		}

		// Generate the report here.
		eval.report = event::conforms
		{
			event, opts.non_conform.report
		};

		// When opts.conforming is false a bad report is not an error.
		if(!opts.conforming)
			return;

		const bool redacted
		{
			// redacted hint given in options
			opts.redacted != -1?
				bool(opts.redacted):

			// assume unredacted when user requires content
			opts.require_content?
				false:

			// assume redacted when hash mismatch already allowed
			(opts.non_conform.has(event::conforms::MISMATCH_HASHES))?
				true:

			// assume no redaction for hash match
			(!eval.report.has(event::conforms::MISMATCH_HASHES))?
				false:

			// make query
				bool(m::redacted(event.event_id))
		};

		auto report
		{
			eval.report
		};;

		// Allow content hash to fail on redacted events.
		if(redacted)
			report.del(event::conforms::MISMATCH_HASHES);

		// Otherwise this will kill the eval
		if(!report.clean())
			throw error
			{
				fault::INVALID, "Non-conforming event: %s",
				string(report)
			};
	}
};

namespace ircd::m
{
	constexpr size_t event_conforms_num{num_of<event::conforms::code>()};
	extern const std::array<string_view, event_conforms_num> event_conforms_reflects;
}

decltype(ircd::m::event_conforms_reflects)
ircd::m::event_conforms_reflects
{
	"INVALID_OR_MISSING_EVENT_ID",
	"INVALID_OR_MISSING_ROOM_ID",
	"INVALID_OR_MISSING_SENDER_ID",
	"MISSING_TYPE",
	"INVALID_TYPE",
	"MISSING_ORIGIN",
	"INVALID_ORIGIN",
	"INVALID_STATE_KEY",
	"INVALID_OR_MISSING_REDACTS_ID",
	"MISSING_CONTENT_MEMBERSHIP",
	"INVALID_CONTENT_MEMBERSHIP",
	"MISSING_MEMBER_STATE_KEY",
	"INVALID_MEMBER_STATE_KEY",
	"MISSING_PREV_EVENTS",
	"MISSING_AUTH_EVENTS",
	"DEPTH_NEGATIVE",
	"DEPTH_ZERO",
	"MISSING_SIGNATURES",
	"MISSING_ORIGIN_SIGNATURE",
	"MISMATCH_ORIGIN_SENDER",
	"MISMATCH_CREATE_SENDER",
	"MISMATCH_ALIASES_STATE_KEY",
	"SELF_REDACTS",
	"SELF_PREV_EVENT",
	"SELF_AUTH_EVENT",
	"DUP_PREV_EVENT",
	"DUP_AUTH_EVENT",
	"MISMATCH_EVENT_ID",
	"MISSING_HASHES",
	"MISMATCH_HASHES",
};

std::ostream &
ircd::m::operator<<(std::ostream &s, const event::conforms &conforms)
{
	thread_local char buf[1024];
	s << conforms.string(buf);
	return s;
}

ircd::string_view
ircd::m::reflect(const event::conforms::code &code)
try
{
	return event_conforms_reflects.at(code);
}
catch(const std::out_of_range &e)
{
	return "??????"_sv;
}

ircd::m::event::conforms::code
ircd::m::event::conforms::reflect(const string_view &name)
{
	const auto it
	{
		std::find(begin(event_conforms_reflects), end(event_conforms_reflects), name)
	};

	if(it == end(event_conforms_reflects))
		throw std::out_of_range
		{
			"There is no event::conforms code by that name."
		};

	return code(std::distance(begin(event_conforms_reflects), it));
}

ircd::m::event::conforms::conforms(const event &e,
                                   const uint64_t &skip)
:conforms{e}
{
	report &= ~skip;
}

ircd::m::event::conforms::conforms(const event &e)
try
:report{0}
{
	if(!e.event_id)
		set(INVALID_OR_MISSING_EVENT_ID);

	if(defined(json::get<"event_id"_>(e)))
		if(!valid(m::id::EVENT, json::get<"event_id"_>(e)))
			set(INVALID_OR_MISSING_EVENT_ID);

	if(!has(INVALID_OR_MISSING_EVENT_ID))
		if(!m::check_id(e))
			set(MISMATCH_EVENT_ID);

	if(empty(json::get<"hashes"_>(e)))
		set(MISSING_HASHES);

	if(!has(MISMATCH_HASHES) && !has(MISSING_HASHES))
		if(!m::verify_hash(e))
			set(MISMATCH_HASHES);

	if(!valid(m::id::ROOM, json::get<"room_id"_>(e)))
		set(INVALID_OR_MISSING_ROOM_ID);

	if(!valid(m::id::USER, json::get<"sender"_>(e)))
		set(INVALID_OR_MISSING_SENDER_ID);

	if(empty(json::get<"type"_>(e)))
		set(MISSING_TYPE);

	if(json::get<"type"_>(e).size() > event::TYPE_MAX_SIZE)
		set(INVALID_TYPE);

	if(empty(json::get<"origin"_>(e)))
		set(MISSING_ORIGIN);

	if(json::get<"origin"_>(e).size() > event::ORIGIN_MAX_SIZE)
		set(INVALID_ORIGIN);

	if(!rfc3986::valid_remote(std::nothrow, json::get<"origin"_>(e)))
		set(INVALID_ORIGIN);

	if(json::get<"state_key"_>(e).size() > event::STATE_KEY_MAX_SIZE)
		set(INVALID_STATE_KEY);

	if(empty(json::get<"signatures"_>(e)))
		set(MISSING_SIGNATURES);

	if(empty(json::object{json::get<"signatures"_>(e).get(json::get<"origin"_>(e))}))
		set(MISSING_ORIGIN_SIGNATURE);

	if(!has(INVALID_OR_MISSING_SENDER_ID))
		if(json::get<"origin"_>(e) != m::id::user{json::get<"sender"_>(e)}.host())
			set(MISMATCH_ORIGIN_SENDER);

	if(json::get<"type"_>(e) == "m.room.create")
		if(m::room::id(json::get<"room_id"_>(e)).host() != m::user::id(json::get<"sender"_>(e)).host())
			set(MISMATCH_CREATE_SENDER);

	if(json::get<"type"_>(e) == "m.room.aliases")
		if(m::user::id(json::get<"sender"_>(e)).host() != json::get<"state_key"_>(e))
			set(MISMATCH_ALIASES_STATE_KEY);

	if(json::get<"type"_>(e) == "m.room.redaction")
		if(!valid(m::id::EVENT, json::get<"redacts"_>(e)))
			set(INVALID_OR_MISSING_REDACTS_ID);

	if(json::get<"redacts"_>(e))
		if(json::get<"redacts"_>(e) == e.event_id)
			set(SELF_REDACTS);

	if(json::get<"type"_>(e) == "m.room.member")
		if(empty(unquote(json::get<"content"_>(e).get("membership"))))
			set(MISSING_CONTENT_MEMBERSHIP);

	if(json::get<"type"_>(e) == "m.room.member")
		if(!all_of<std::islower>(unquote(json::get<"content"_>(e).get("membership"))))
			set(INVALID_CONTENT_MEMBERSHIP);

	if(json::get<"type"_>(e) == "m.room.member")
		if(empty(json::get<"state_key"_>(e)))
			set(MISSING_MEMBER_STATE_KEY);

	if(json::get<"type"_>(e) == "m.room.member")
		if(!valid(m::id::USER, json::get<"state_key"_>(e)))
			set(INVALID_MEMBER_STATE_KEY);

	if(json::get<"type"_>(e) != "m.room.create")
		if(empty(json::get<"prev_events"_>(e)))
			set(MISSING_PREV_EVENTS);

	if(json::get<"type"_>(e) != "m.room.create")
		if(empty(json::get<"auth_events"_>(e)))
			set(MISSING_AUTH_EVENTS);

	if(json::get<"depth"_>(e) != json::undefined_number && json::get<"depth"_>(e) < 0)
		set(DEPTH_NEGATIVE);

	if(json::get<"type"_>(e) != "m.room.create")
		if(json::get<"depth"_>(e) == 0)
			set(DEPTH_ZERO);

	const event::prev prev{e};
	if(json::get<"event_id"_>(e))
	{
		for(size_t i(0); i < prev.auth_events_count(); ++i)
			if(prev.auth_event(i) == json::get<"event_id"_>(e))
				set(SELF_AUTH_EVENT);

		for(size_t i(0); i < prev.prev_events_count(); ++i)
			if(prev.prev_event(i) == json::get<"event_id"_>(e))
				set(SELF_PREV_EVENT);
	}

	for(size_t i(0); i < prev.auth_events_count(); ++i)
	{
		const auto &[event_id, ref_hash]
		{
			prev.auth_events(i)
		};

		for(size_t j(0); j < prev.auth_events_count(); ++j)
			if(i != j)
				if(event_id == prev.auth_event(j))
					set(DUP_AUTH_EVENT);
	}

	for(size_t i(0); i < prev.prev_events_count(); ++i)
	{
		const auto &[event_id, ref_hash]
		{
			prev.prev_events(i)
		};

		for(size_t j(0); j < prev.prev_events_count(); ++j)
			if(i != j)
				if(event_id == prev.prev_event(j))
					set(DUP_PREV_EVENT);
	}
}
catch(const std::exception &_e)
{
	log::error
	{
		log, "Unable to complete conformity check :%s",
		_e.what(),
	};

	throw;
}

void
ircd::m::event::conforms::operator|=(const code &code)
&
{
	set(code);
}

void
ircd::m::event::conforms::del(const code &code)
{
	report &= ~(1UL << code);
}

void
ircd::m::event::conforms::set(const code &code)
{
	report |= (1UL << code);
}

ircd::string_view
ircd::m::event::conforms::string(const mutable_buffer &out)
const
{
	mutable_buffer buf{out};
	for(uint64_t i(0); i < num_of<code>(); ++i)
	{
		if(!has(code(i)))
			continue;

		if(begin(buf) != begin(out))
			consume(buf, copy(buf, ' '));

		consume(buf, copy(buf, m::reflect(code(i))));
	}

	return { data(out), begin(buf) };
}

bool
ircd::m::event::conforms::has(const code &code)
const
{
	return report & (1UL << code);
}

bool
ircd::m::event::conforms::has(const uint &code)
const
{
	return (report & (1UL << code)) == code;
}

bool
ircd::m::event::conforms::operator!()
const
{
	return clean();
}

ircd::m::event::conforms::operator bool()
const
{
	return !clean();
}

bool
ircd::m::event::conforms::clean()
const
{
	return report == 0;
}