/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */

#include "remote/httpmessage.hpp"
#include "base/json.hpp"
#include "remote/httputility.hpp"
#include "remote/url.hpp"
#include <boost/beast/http.hpp>
#include <fstream>
#include <string>

using namespace icinga;

/**
 * Adapter class for Boost Beast HTTP messages body to be used with the @c JsonEncoder.
 *
 * This class implements the @c nlohmann::detail::output_adapter_protocol<> interface and provides
 * a way to write JSON data directly into the body of a @c HttpResponse.
 *
 * @ingroup base
 */
class HttpResponseJsonWriter : public AsyncJsonWriter
{
public:
	explicit HttpResponseJsonWriter(HttpResponse& msg) : m_Message{msg}
	{
		m_Message.body().Start();
#if BOOST_VERSION >= 107000
		m_Message.body().Buffer().reserve(m_MinPendingBufferSize);
#endif /* BOOST_VERSION */
	}

	~HttpResponseJsonWriter() override { m_Message.body().Finish(); }

	void write_character(char c) override { write_characters(&c, 1); }

	void write_characters(const char* s, std::size_t length) override
	{
		auto buf = m_Message.body().Buffer().prepare(length);
		boost::asio::buffer_copy(buf, boost::asio::const_buffer{s, length});
		m_Message.body().Buffer().commit(length);
	}

	void MayFlush(boost::asio::yield_context& yield) override
	{
		if (m_Message.body().Size() >= m_MinPendingBufferSize) {
			m_Message.Flush(yield);
		}
	}

private:
	HttpResponse& m_Message;
	// Minimum size of the pending buffer before we flush the data to the underlying stream
	static constexpr std::size_t m_MinPendingBufferSize = 4096;
};

HttpRequest::HttpRequest(Shared<AsioTlsStream>::Ptr stream) : m_Stream(std::move(stream))
{
}

void HttpRequest::ParseHeader(boost::beast::flat_buffer& buf, boost::asio::yield_context yc)
{
	boost::beast::http::async_read_header(*m_Stream, buf, m_Parser, yc);
	base() = m_Parser.get().base();
}

void HttpRequest::ParseBody(boost::beast::flat_buffer& buf, boost::asio::yield_context yc)
{
	boost::beast::http::async_read(*m_Stream, buf, m_Parser, yc);
	body() = std::move(m_Parser.release().body());
}

ApiUser::Ptr HttpRequest::User() const
{
	return m_User;
}

void HttpRequest::User(const ApiUser::Ptr& user)
{
	m_User = user;
}

Url::Ptr HttpRequest::Url() const
{
	return m_Url;
}

void HttpRequest::DecodeUrl()
{
	m_Url = new icinga::Url(std::string(target()));
}

Dictionary::Ptr HttpRequest::Params() const
{
	return m_Params;
}

void HttpRequest::DecodeParams()
{
	if (!m_Url) {
		DecodeUrl();
	}
	m_Params = HttpUtility::FetchRequestParameters(m_Url, body());
}

HttpResponse::HttpResponse(Shared<AsioTlsStream>::Ptr stream) : m_Stream(std::move(stream))
{
}

void HttpResponse::Flush(boost::asio::yield_context yc)
{
	if (!chunked() && !has_content_length()) {
		ASSERT(!m_Serializer.is_header_done());
		prepare_payload();
	}

	boost::system::error_code ec;
	boost::beast::http::async_write(*m_Stream, m_Serializer, yc[ec]);
	if (ec && ec != boost::beast::http::error::need_buffer) {
		if (yc.ec_) {
			*yc.ec_ = ec;
			return;
		} else {
			BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
		}
	}
	m_Stream->async_flush(yc);

	ASSERT(m_Serializer.is_done() || !body().Finished());
}

void HttpResponse::StartStreaming()
{
	ASSERT(body().Size() == 0 && !m_Serializer.is_header_done());
	body().Start();
	chunked(true);
}

void HttpResponse::SendFile(const String& path, boost::asio::yield_context yc)
{
	static constexpr std::size_t minChunkSize = 4096;

	std::ifstream fp(path.CStr(), std::ifstream::in | std::ifstream::binary | std::ifstream::ate);
	fp.exceptions(std::ifstream::badbit | std::ifstream::eofbit);

	std::size_t remaining = fp.tellg();
	fp.seekg(0);

	content_length(remaining);
	body().Start();

	while (remaining) {
		auto maxTransfer = remaining > minChunkSize ? minChunkSize : remaining;

		auto buf = *body().Buffer().prepare(maxTransfer).begin();
		fp.read(static_cast<char*>(buf.data()), buf.size());
		body().Buffer().commit(buf.size());

		remaining -= buf.size();
		Flush(yc);
	}
}

JsonEncoder HttpResponse::GetJsonEncoder(bool pretty)
{
	return JsonEncoder{std::make_shared<HttpResponseJsonWriter>(*this), pretty};
}
