// 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.

using namespace ircd;

[[noreturn]] static void rethrow(const std::exception_ptr &, const m::user &, const string_view &);
static std::exception_ptr fetch_profile_remote(const m::user &, const string_view &);
static m::resource::response get__profile(client &, const m::resource::request &);
static m::resource::response put__profile(client &, const m::resource::request &);

mapi::header
IRCD_MODULE
{
	"Client 8.2 :Profiles"
};

const size_t
PARAM_MAX_SIZE
{
	128
};

ircd::m::resource
profile_resource
{
	"/_matrix/client/r0/profile/",
	{
		"(8.2) Profiles",
		resource::DIRECTORY,
	}
};

m::resource::method
method_get
{
	profile_resource, "GET", get__profile
};

m::resource::method
method_put
{
	profile_resource, "PUT", put__profile,
	{
		method_put.REQUIRES_AUTH |
		method_put.RATE_LIMITED
	}
};

m::resource::response
put__profile(client &client,
              const m::resource::request &request)
{
	if(request.parv.size() < 1)
		throw m::NEED_MORE_PARAMS
		{
			"user_id path parameter required"
		};

	if(request.parv.size() < 2)
		throw m::NEED_MORE_PARAMS
		{
			"profile property path parameter required"
		};

	m::user::id::buf user_id
	{
		url::decode(user_id, request.parv[0])
	};

	if(user_id != request.user_id)
		throw m::FORBIDDEN
		{
			"Trying to set profile for '%s' but you are '%s'",
			user_id,
			request.user_id
		};

	const m::user user
	{
		user_id
	};

	char parambuf[PARAM_MAX_SIZE];
	const string_view &param
	{
		url::decode(parambuf, request.parv[1])
	};

	const string_view &value
	{
		request.at(param)
	};

	const m::user::profile profile
	{
		user
	};

	bool modified{true};
	profile.get(std::nothrow, param, [&value, &modified]
	(const string_view &param, const string_view &existing) noexcept
	{
		modified = existing != value;
	});

	if(!modified)
		return m::resource::response
		{
			client, http::OK
		};

	const auto eid
	{
		profile.set(param, value)
	};

	return m::resource::response
	{
		client, http::OK
	};
}

m::resource::response
get__profile(client &client,
             const m::resource::request &request)
{
	if(request.parv.size() < 1)
		throw m::NEED_MORE_PARAMS
		{
			"user_id path parameter required"
		};

	m::user::id::buf user_id
	{
		url::decode(user_id, request.parv[0])
	};

	const m::user user
	{
		user_id
	};

	char parambuf[PARAM_MAX_SIZE];
	const string_view &param
	{
		request.parv.size() > 1?
			url::decode(parambuf, request.parv[1]):
			string_view{}
	};

	// For remote users, we try to get the latest profile data
	// from the remote server and cache it locally. When there's
	// a problem, we store that problem in this eptr for later.
	const std::exception_ptr eptr
	{
		!my(user)?
			fetch_profile_remote(user, param):
			std::exception_ptr{}
	};

	// Now we treat the profile as local data in any case
	const m::user::profile profile
	{
		user
	};

	if(param) try
	{
		// throws if param not found
		profile.get(param, [&client]
		(const string_view &param, const string_view &value)
		{
			m::resource::response
			{
				client, json::members
				{
					{ param, value }
				}
			};
		});

		return {};
	}
	catch(const std::exception &e)
	{
		// If there was a problem querying locally for this param and the
		// user is remote, eptr will have a better error for the client.
		if(eptr && !my(user))
			rethrow(eptr, user, param);

		throw;
	}

	// Have to return a 404 if the profile is empty rather than a {},
	// so we iterate for at least one element first to check that.
	const bool empty
	{
		profile.for_each([]
		(const string_view &, const string_view &) noexcept
		{
			return false;
		})
	};

	// If we have no profile data and the user is not ours, eptr might have
	// a better error for our client.
	if(empty && eptr && !my(user))
		rethrow(eptr, user, param);

	// Riot throws uncaught exceptions when its own empty profile 404's.
	if(empty && request.user_id == user_id)
		return resource::response
		{
			client, http::OK
		};

	// Otherwise if there's no profile data we 404 our client.
	if(empty)
		throw m::NOT_FOUND
		{
			"Profile for %s is empty.",
			string_view{user.user_id}
		};

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

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

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

	profile.for_each([&top]
	(const string_view &param, const string_view &value)
	{
		json::stack::member
		{
			top, param, value
		};

		return true;
	});

	return {};
}

std::exception_ptr
fetch_profile_remote(const m::user &user,
                     const string_view &key)
try
{
	m::user::profile::fetch(user, user.user_id.host(), key);
	return {};
}
catch(const std::exception &e)
{
	return std::current_exception();
}

void
rethrow(const std::exception_ptr &eptr,
        const m::user &user,
        const string_view &key)
try
{
	assert(bool(eptr));
	std::rethrow_exception(eptr);
}
catch(const http::error &e)
{
	throw m::error
	{
		e.code, "M_UNKNOWN",
		"Server '%s' responded to profile request for %s with :%s",
		user.user_id.host(),
		string_view{user.user_id},
		e.content
	};
}
catch(const ctx::timeout &)
{
	throw m::error
	{
		http::GATEWAY_TIMEOUT, "M_PROFILE_TIMEOUT",
		"Server '%s' did not respond with profile for %s in time.",
		user.user_id.host(),
		string_view{user.user_id}
	};
}
catch(const server::unavailable &e)
{
	throw m::error
	{
		http::SERVICE_UNAVAILABLE, "M_PROFILE_UNAVAILABLE",
		"Server '%s' cannot be contacted for profile of %s :%s",
		user.user_id.host(),
		string_view{user.user_id},
		e.what()
	};
}
catch(const server::error &e)
{
	throw m::error
	{
		http::BAD_GATEWAY, "M_PROFILE_UNAVAILABLE",
		"Error when contacting '%s' for profile of %s :%s",
		user.user_id.host(),
		string_view{user.user_id},
		e.what()
	};
}
