// The Construct
//
// Copyright (C) The Construct Developers, Authors & Contributors
// Copyright (C) 2016-2020 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.

#include "room_keys.h"

namespace ircd::m
{
	static resource::response _get_room_keys_keys(client &, const resource::request &, const room::state &, const event::idx &, const string_view &, const string_view &);
	static void _get_room_keys_keys(client &, const resource::request &, const room::state &, const event::idx &, const string_view &, json::stack::object &);
	static resource::response get_room_keys_keys(client &, const resource::request &);
	extern resource::method room_keys_keys_get;

	static event::id::buf put_room_keys_keys_key(client &, const resource::request &, const room::id &, const string_view &, const event::idx &, const json::object &);
	static resource::response put_room_keys_keys(client &, const resource::request &);
	extern resource::method room_keys_keys_put;

	static event::id::buf delete_room_keys_key(client &, const resource::request &, const room &, const event::idx &);
	static event::id::buf delete_room_keys_key(client &, const resource::request &, const room &, const room::id &, const string_view &, const event::idx &);
	static resource::response delete_room_keys_keys(client &, const resource::request &);
	extern resource::method room_keys_keys_delete;

	extern resource room_keys_keys;
}

decltype(ircd::m::room_keys_keys)
ircd::m::room_keys_keys
{
	"/_matrix/client/unstable/room_keys/keys",
	{
		"(undocumented) Room Keys Keys",
		resource::DIRECTORY,
	}
};

//
// DELETE
//

decltype(ircd::m::room_keys_keys_delete)
ircd::m::room_keys_keys_delete
{
	room_keys_keys, "DELETE", delete_room_keys_keys,
	{
		room_keys_keys_delete.REQUIRES_AUTH |
		room_keys_keys_delete.RATE_LIMITED
	}
};

ircd::m::resource::response
ircd::m::delete_room_keys_keys(client &client,
                               const resource::request &request)
{
	char room_id_buf[room::id::buf::SIZE];
	const string_view &room_id
	{
		request.parv.size() > 0?
			url::decode(room_id_buf, request.parv[0]):
			string_view{}
	};

	char session_id_buf[256];
	const string_view &session_id
	{
		request.parv.size() > 1?
			url::decode(session_id_buf, request.parv[1]):
			string_view{}
	};

	const event::idx version
	{
		request.query.get<event::idx>("version", 0)
	};

	const m::user::room user_room
	{
		request.user_id
	};

	const m::room::state state
	{
		user_room
	};

	if(!room_id && !session_id)
	{
		state.for_each("ircd.room_keys.key", [&client, &request, &user_room, &version]
		(const string_view &, const string_view &state_key, const event::idx &event_idx)
		{
			const auto &[room_id, session_id, _version]
			{
				unmake_state_key(state_key)
			};

			if(version && _version != lex_cast(version))
				return true;

			delete_room_keys_key(client, request, user_room, event_idx);
			return true;
		});
	}
	else if(!session_id)
	{
		state.for_each("ircd.room_keys.key", [&client, &request, &user_room, &version, &room_id]
		(const string_view &, const string_view &state_key, const event::idx &event_idx)
		{
			const auto &[_room_id, session_id, _version]
			{
				unmake_state_key(state_key)
			};

			if(version && _version != lex_cast(version))
				return true;

			if(_room_id != room_id)
				return true;

			delete_room_keys_key(client, request, user_room, event_idx);
			return true;
		});
	}
	else delete_room_keys_key(client, request, user_room, room_id, session_id, version);

	const auto &[count, etag]
	{
		count_etag(state, version)
	};

	const json::value _etag
	{
		lex_cast(etag), json::STRING
	};

	return resource::response
	{
		client, json::members
		{
			{ "count", count },
			{ "etag",  _etag },
		}
	};
}

ircd::m::event::id::buf
ircd::m::delete_room_keys_key(client &client,
                              const resource::request &request,
                              const room &user_room,
                              const room::id &room_id,
                              const string_view &session_id,
                              const event::idx &version)
{
	char state_key_buf[event::STATE_KEY_MAX_SIZE];
	const string_view state_key
	{
		make_state_key(state_key_buf, room_id, session_id, version)
	};

	const room::state state
	{
		user_room
	};

	const auto event_idx
	{
		state.get(std::nothrow, "ircd.room_keys.key", state_key)
	};

	if(!event_idx)
		return {};

	return delete_room_keys_key(client, request, user_room, event_idx);
}

ircd::m::event::id::buf
ircd::m::delete_room_keys_key(client &client,
                              const resource::request &request,
                              const room &user_room,
                              const event::idx &event_idx)
{
	const auto event_id
	{
		m::event_id(event_idx)
	};

	const auto redact_id
	{
		m::redact(user_room, request.user_id, event_id, "deleted by client")
	};

	return redact_id;
}

//
// PUT
//

decltype(ircd::m::room_keys_keys_put)
ircd::m::room_keys_keys_put
{
	room_keys_keys, "PUT", put_room_keys_keys,
	{
		// Flags
		room_keys_keys_put.REQUIRES_AUTH |
		room_keys_keys_put.RATE_LIMITED,

		// timeout //TODO: XXX designated
		30s,

		// Payload maximum
		1_MiB,
	}
};

ircd::m::resource::response
ircd::m::put_room_keys_keys(client &client,
                            const resource::request &request)
{
	char room_id_buf[room::id::buf::SIZE];
	const string_view &room_id
	{
		request.parv.size() > 0?
			url::decode(room_id_buf, request.parv[0]):
			string_view{}
	};

	char session_id_buf[256];
	const string_view &session_id
	{
		request.parv.size() > 1?
			url::decode(session_id_buf, request.parv[1]):
			string_view{}
	};

	const event::idx version
	{
		request.query.at<event::idx>("version")
	};

	const m::user::room user_room
	{
		request.user_id
	};

	const m::room::state state
	{
		user_room
	};

	if(!room_id && !session_id)
	{
		const json::object &rooms
		{
			request["rooms"]
		};

		for(const auto &[room_id, room_data] : rooms)
		{
			const json::object sessions
			{
				json::object(room_data)["sessions"]
			};

			for(const auto &[session_id, session] : sessions)
				put_room_keys_keys_key(client, request, room_id, session_id, version, session);
		}
	}
	else if(!session_id)
	{
		const json::object &sessions
		{
			request["sessions"]
		};

		for(const auto &[session_id, session] : sessions)
			put_room_keys_keys_key(client, request, room_id, session_id, version, session);
	}
	else put_room_keys_keys_key(client, request, room_id, session_id, version, request);

	const auto &[count, etag]
	{
		count_etag(state, version)
	};

	const json::value _etag
	{
		lex_cast(etag), json::STRING
	};

	return resource::response
	{
		client, json::members
		{
			{ "count", count },
			{ "etag",  _etag },
		}
	};
}

ircd::m::event::id::buf
ircd::m::put_room_keys_keys_key(client &client,
                                const resource::request &request,
                                const room::id &room_id,
                                const string_view &session_id,
                                const event::idx &version,
                                const json::object &content)
{
	const m::user::room user_room
	{
		request.user_id
	};

	const m::room::type events
	{
		user_room, "ircd.room_keys.version"
	};

	events.for_each([&version]
	(const auto &, const auto &, const event::idx &_event_idx)
	{
		if(m::redacted(_event_idx))
			return true;

		if(_event_idx != version)
			throw http::error
			{
				"%lu is not the most recent key version",
				http::FORBIDDEN,
				version
			};

		return false; // false to break after this first hit
	});

	char state_key_buf[event::STATE_KEY_MAX_SIZE];
	const string_view state_key
	{
		make_state_key(state_key_buf, room_id, session_id, version)
	};

	const auto event_id
	{
		send(user_room, request.user_id, "ircd.room_keys.key", state_key, content)
	};

	return event_id;
}

//
// GET
//

decltype(ircd::m::room_keys_keys_get)
ircd::m::room_keys_keys_get
{
	room_keys_keys, "GET", get_room_keys_keys,
	{
		room_keys_keys_get.REQUIRES_AUTH |
		room_keys_keys_get.RATE_LIMITED
	}
};

ircd::m::resource::response
ircd::m::get_room_keys_keys(client &client,
                            const resource::request &request)
{
	char room_id_buf[room::id::buf::SIZE];
	const string_view &room_id
	{
		request.parv.size() > 0?
			url::decode(room_id_buf, request.parv[0]):
			string_view{}
	};

	char session_id_buf[256];
	const string_view &session_id
	{
		request.parv.size() > 1?
			url::decode(session_id_buf, request.parv[1]):
			string_view{}
	};

	const event::idx version
	{
		request.query.at<event::idx>("version")
	};

	const m::user::room user_room
	{
		request.user_id
	};

	const m::room::state state
	{
		user_room
	};

	if(room_id && session_id)
		return _get_room_keys_keys(client, request, state, version, room_id, session_id);

	resource::response::chunked response
	{
		client, http::OK
	};

	json::stack out
	{
		response.buf, response.flusher()
	};

	json::stack::object top
	{
		out
	};

	json::stack::object rooms
	{
		top, "rooms"
	};

	if(room_id)
	{
		_get_room_keys_keys(client, request, state, version, room_id, rooms);
		return response;
	}

	m::room::id::buf last_room;
	state.for_each("ircd.room_keys.key", [&client, &request, &state, &version, &rooms, &last_room]
	(const string_view &, const string_view &state_key, const event::idx &)
	{
		const auto &[room_id, _session_id, _version]
		{
			unmake_state_key(state_key)
		};

		if(_version != lex_cast(version))
			return true;

		if(room_id == last_room)
			return true;

		_get_room_keys_keys(client, request, state, version, room_id, rooms);
		return true;
	});

	return response;
}

void
ircd::m::_get_room_keys_keys(client &client,
                             const resource::request &request,
                             const m::room::state &state,
                             const event::idx &version,
                             const string_view &room_id,
                             json::stack::object &rooms)
{
	json::stack::object room
	{
		rooms, room_id
	};

	json::stack::object sessions
	{
		room, "sessions"
	};

	state.for_each("ircd.room_keys.key", [&room_id, &version, &sessions]
	(const string_view &type, const string_view &state_key, const event::idx &event_idx)
	{
		const auto &[_room_id, _session_id, _version]
		{
			unmake_state_key(state_key)
		};

		if(_room_id != room_id)
			return true;

		if(_version != lex_cast(version))
			return true;

		const string_view &session_id
		{
			_session_id
		};

		m::get(std::nothrow, event_idx, "content", [&sessions, &session_id]
		(const json::object &session)
		{
			json::stack::member
			{
				sessions, session_id, session
			};
		});

		return true;
	});
}

ircd::m::resource::response
ircd::m::_get_room_keys_keys(client &client,
                             const resource::request &request,
                             const m::room::state &state,
                             const event::idx &version,
                             const string_view &room_id,
                             const string_view &session_id)
{
	char state_key_buf[event::STATE_KEY_MAX_SIZE];
	const string_view state_key
	{
		make_state_key(state_key_buf, room_id, session_id, version)
	};

	const auto event_idx
	{
		state.get("ircd.room_keys.key", state_key)
	};

	m::get(event_idx, "content", [&client]
	(const json::object &content)
	{
		resource::response
		{
			client, content
		};
	});

	return {}; // responded from closure or thrown
}
