// Copyright Maarten L. Hekkelman, Radboud University 2008-2013.
//        Copyright Maarten L. Hekkelman, 2014-2026
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include "zeep/http/request.hpp"

#include "zeep/el/object.hpp"
#include "zeep/streambuf.hpp"
#include "zeep/unicode-support.hpp"

#include <algorithm>
#include <cctype>
#include <cstring>
#include <exception>
#include <ios>
#include <regex>
#include <sstream>
#include <stdexcept>

namespace zeep::http
{

request::request(std::string method, uri uri, std::tuple<int, int> version,
	std::vector<header> &&headers, std::string &&payload)
	: m_method(std::move(method))
	, m_uri(std::move(uri))
	, m_version({ static_cast<char>('0' + std::get<0>(version)), '.', static_cast<char>('0' + std::get<1>(version)) })
	, m_headers(std::move(headers))
	, m_payload(std::move(payload))
{
}

void swap(request &lhs, request &rhs) noexcept
{
	if (&lhs != &rhs)
	{
		std::swap(lhs.m_local_address, rhs.m_local_address);
		std::swap(lhs.m_local_port, rhs.m_local_port);
		std::swap(lhs.m_method, rhs.m_method);
		std::swap(lhs.m_uri, rhs.m_uri);
		std::swap(lhs.m_version, rhs.m_version);
		std::swap(lhs.m_headers, rhs.m_headers);
		std::swap(lhs.m_payload, rhs.m_payload);
		std::swap(lhs.m_close, rhs.m_close);
		std::swap(lhs.m_timestamp, rhs.m_timestamp);
		std::swap(lhs.m_credentials, rhs.m_credentials);
		std::swap(lhs.m_remote_address, rhs.m_remote_address);
	}
}

float request::get_accept(std::string_view type) const
{
	float result = 1.0f;

#define IDENT "[-+.a-z0-9]+"
#define TYPE "\\*|" IDENT
#define MEDIARANGE "\\s*(" TYPE ")/(" TYPE ").*?(?:;\\s*q=(\\d(?:\\.\\d?)?))?"

	static std::regex rx(MEDIARANGE);

	if (type.empty())
		return 1.0;

	std::string t1(type), t2;
	std::string::size_type s = t1.find('/');
	if (s != std::string::npos)
	{
		t2 = t1.substr(s + 1);
		t1.erase(s, t1.length() - s);
	}

	for (const header &h : m_headers)
	{
		if (not iequals(h.name, "Accept"))
			continue;

		result = 0;

		std::string::size_type b = 0, e = h.value.find(',');
		for (;;)
		{
			if (e == std::string::npos)
				e = h.value.length();

			std::string mediarange = h.value.substr(b, e - b);

			std::smatch m;
			if (std::regex_search(mediarange, m, rx))
			{
				std::string type1 = m[1].str();
				std::string type2 = m[2].str();

				float value = 1.0f;
				if (m[3].matched)
					value = std::stof(m[3].str());

				if (type1 == t1 and type2 == t2)
				{
					result = value;
					break;
				}

				if ((type1 == t1 and type2 == "*") or
					(type1 == "*" and type2 == "*"))
				{
					if (result < value)
						result = value;
				}
			}

			if (e == h.value.length())
				break;

			b = e + 1;
			while (b < h.value.length() and isspace(h.value[b]))
				++b;
			e = h.value.find(',', b);
		}

		break;
	}

	return result;
}

// m_request.http_version_minor >= 1 and not m_request.close

bool request::keep_alive() const
{
	return get_version() >= std::make_tuple(1, 1) and
	       iequals(get_header("Connection"), "keep-alive");
}

void request::set_header(std::string name, std::string value)
{
	bool replaced = false;

	for (header &h : m_headers)
	{
		if (not iequals(h.name, name))
			continue;

		h.value = value;
		replaced = true;
		break;
	}

	if (not replaced)
		m_headers.emplace_back(std::move(name), std::move(value));
}

std::string request::get_header(std::string_view name) const
{
	std::string result;

	for (const header &h : m_headers)
	{
		if (not iequals(h.name, name))
			continue;

		result = h.value;
		break;
	}

	return result;
}

void request::remove_header(std::string_view name)
{
	std::erase_if(m_headers,
		[name](const header &h) -> bool
		{ return h.name == name; });
}

std::pair<std::string, bool> get_urldecoded_parameter(std::string_view s, std::string_view name)
{
	std::string::size_type b = 0;
	std::string result;
	bool found = false;
	size_t nlen = name.length();

	while (b != std::string::npos)
	{
		std::string::size_type e = s.find_first_of("&;", b);
		std::string::size_type n = (e == std::string::npos) ? s.length() - b : e - b;

		if ((n == nlen or (n > nlen + 1 and s[b + nlen] == '=')) and name == s.substr(b, nlen))
		{
			found = true;

			if (n == nlen)
				result = name; // what else?
			else
			{
				b += nlen + 1;
				result = s.substr(b, e - b);
				result = decode_url(result);
			}

			break;
		}

		b = e == std::string::npos ? e : e + 1;
	}

	return std::make_pair(result, found);
}

std::optional<std::string> request::get_parameter(std::string_view name) const
{
	std::string result, contentType = get_header("Content-Type");
	bool found = false;

	if (starts_with(contentType, "application/x-www-form-urlencoded"))
	{
		tie(result, found) = get_urldecoded_parameter(m_payload, name);
		if (found)
			return result;
	}

	auto query = m_uri.get_query(false);

	if (not query.empty())
	{
		tie(result, found) = get_urldecoded_parameter(query, name);
		if (found)
			return result;
	}

	if (starts_with(contentType, "application/json"))
	{
		try
		{
			char_streambuf buf(m_payload.data(), m_payload.length());
			std::istream is(&buf);
			el::object e;
			deserialize(is, e);

			if (e.is_object() and e.contains(name))
			{
				result = e.at(std::string{ name }).get<std::string>();
				found = true;
			}
		}
		catch (const std::exception &)
		{
			found = false;
		}
	}
	else if (starts_with(contentType, "multipart/form-data"))
	{
		std::string::size_type b = contentType.find("boundary=");
		if (b != std::string::npos)
		{
			std::string boundary = contentType.substr(b + strlen("boundary="));

			enum
			{
				START,
				HEADER,
				CONTENT,
				SKIP
			} state = SKIP;

			std::string contentName;
			std::regex rx("content-disposition:\\s*form-data;.*?\\bname=\"([^\"]+)\".*", std::regex::icase);
			std::smatch m;

			std::string::difference_type i = 0, r = 0, l = 0;

			for (i = 0; i <= static_cast<decltype(i)>(m_payload.length()); ++i)
			{
				if (m_payload[i] != '\r' and m_payload[i] != '\n')
					continue;

				// we have found a 'line' at [l, i)
				if (m_payload.compare(l, 2, "--") == 0 and
					m_payload.compare(l + 2, boundary.length(), boundary) == 0)
				{
					// if we're in the content state or if this is the last line
					if (state == CONTENT or m_payload.compare(l + 2 + boundary.length(), 2, "--") == 0)
					{
						if (r > 0)
						{
							auto n = l - r;
							if (n >= 1 and m_payload[r + n - 1] == '\n')
								--n;
							if (n >= 1 and m_payload[r + n - 1] == '\r')
								--n;

							result.assign(m_payload, r, n);
						}

						break;
					}

					// Not the last, so it must be a separator and we're now in the Header part
					state = HEADER;
				}
				else if (state == HEADER)
				{
					if (l == i) // empty line
					{
						if (contentName == name)
						{
							found = true;
							state = CONTENT;

							r = i + 1;
							if (m_payload[i] == '\r' and m_payload[i + 1] == '\n')
								r = i + 2;
						}
						else
							state = SKIP;
					}
					else if (std::regex_match(m_payload.begin() + l, m_payload.begin() + i, m, rx))
						contentName = m[1].str();
				}

				if (m_payload[i] == '\r' and m_payload[i + 1] == '\n')
					++i;

				l = i + 1;
			}
		}
	}

	if (found)
		return result;

	return {};
}

std::multimap<std::string, std::string> request::get_parameters() const
{
	std::string ps;

	if (m_method == "POST")
	{
		std::string contentType = get_header("Content-Type");

		if (starts_with(contentType, "application/x-www-form-urlencoded"))
			ps = m_payload;
	}
	else if (m_method == "GET" or m_method == "PUT")
		ps = m_uri.get_query(false);

	std::multimap<std::string, std::string> parameters;

	while (not ps.empty())
	{
		std::string::size_type e = ps.find_first_of("&;");
		std::string param;

		if (e != std::string::npos)
		{
			param = ps.substr(0, e);
			ps.erase(0, e + 1);
		}
		else
			std::swap(param, ps);

		if (not param.empty())
		{
			std::string name, value;

			std::string::size_type d = param.find('=');
			if (d != std::string::npos)
			{
				name = param.substr(0, d);
				value = param.substr(d + 1);
			}

			parameters.emplace(decode_url(name), decode_url(value));
		}
	}

	return parameters;
}

struct file_param_parser
{
	file_param_parser(const request &req, const std::string &payload, std::string name);

	file_param next();

	const request &m_req;
	const std::string m_name;
	const std::string &m_payload;
	std::string m_boundary;
	static const std::regex k_rx_disp, k_rx_cont;
	enum
	{
		START,
		HEADER,
		CONTENT,
		SKIP
	} m_state = SKIP;
	std::string::difference_type m_i = 0;
};

const std::regex file_param_parser::k_rx_disp(R"x(content-disposition:\s*form-data(;.+))x", std::regex::icase);
const std::regex file_param_parser::k_rx_cont(R"x(content-type:\s*(\S+/[^;]+)(;.*)?)x", std::regex::icase);

file_param_parser::file_param_parser(const request &req, const std::string &payload, std::string name)
	: m_req(req)
	, m_name(std::move(name))
	, m_payload(payload)
{
	std::string contentType = m_req.get_header("Content-Type");

	if (starts_with(contentType, "multipart/form-data"))
	{
		std::string::size_type b = contentType.find("boundary=");
		if (b != std::string::npos)
			m_boundary = contentType.substr(b + strlen("boundary="));
	}
}

file_param file_param_parser::next()
{
	if (m_boundary.empty())
		return {};

	std::string contentName;
	std::smatch m;

	std::string::difference_type r = 0, l = 0;
	file_param result = {};
	bool found = false;

	for (; m_i <= static_cast<decltype(m_i)>(m_payload.length()); ++m_i)
	{
		if (m_payload[m_i] != '\r' and m_payload[m_i] != '\n')
			continue;

		// we have found a 'line' at [l, i)
		if (m_payload.compare(l, 2, "--") == 0 and
			m_payload.compare(l + 2, m_boundary.length(), m_boundary) == 0)
		{
			// if we're in the content state or if this is the last line
			if (m_state == CONTENT or m_payload.compare(l + 2 + m_boundary.length(), 2, "--") == 0)
			{
				if (r > 0)
				{
					auto n = l - r;
					if (n >= 1 and m_payload[r + n - 1] == '\n')
						--n;
					if (n >= 1 and m_payload[r + n - 1] == '\r')
						--n;

					result.data = m_payload.data() + r;
					result.length = n;
				}

				m_state = HEADER;
				break;
			}

			// Not the last, so it must be a separator and we're now in the Header part
			m_state = HEADER;
		}
		else if (m_state == HEADER)
		{
			if (l == m_i) // empty line
			{
				if (contentName == m_name)
				{
					m_state = CONTENT;
					found = true;

					r = m_i + 1;
					if (m_payload[m_i] == '\r' and m_payload[m_i + 1] == '\n')
						r = m_i + 2;
				}
				else
				{
					result = {};
					m_state = SKIP;
				}
			}
			else if (std::regex_match(m_payload.begin() + l, m_payload.begin() + m_i, m, k_rx_disp))
			{
				auto p = m[1].str();
				std::regex re(R"rx(;\s*(\w+)=("[^"]*"|'[^']*'|\w+))rx");

				auto b = p.begin();
				auto e = p.end();
				std::match_results<std::string::iterator> m2;
				while (b < e and std::regex_search(b, e, m2, re))
				{
					auto key = m2[1].str();
					auto value = m2[2].str();
					if (value.length() > 1 and ((value.front() == '"' and value.back() == '"') or (value.front() == '\'' and value.back() == '\'')))
						value = value.substr(1, value.length() - 2);

					if (key == "name")
						contentName = value;
					else if (key == "filename")
						result.filename = value;

					b = m2[0].second;
				}
			}
			else if (std::regex_match(m_payload.begin() + l, m_payload.begin() + m_i, m, k_rx_cont))
			{
				result.mimetype = m[1].str();
				if (starts_with(result.mimetype, "multipart/"))
					throw std::runtime_error("multipart file uploads are not supported");
			}
		}

		if (m_payload[m_i] == '\r' and m_payload[m_i + 1] == '\n')
			++m_i;

		l = m_i + 1;
	}

	if (not found)
		result = {};

	return result;
}

file_param request::get_file_parameter(std::string name) const
{
	file_param_parser fpp(*this, m_payload, std::move(name));
	return fpp.next();
}

std::vector<file_param> request::get_file_parameters(std::string name) const
{
	file_param_parser fpp(*this, m_payload, std::move(name));

	std::vector<file_param> result;
	for (;;)
	{
		auto fp = fpp.next();
		if (not fp)
			break;
		result.push_back(fp);
	}

	return result;
}

std::string request::get_cookie(std::string_view name) const
{
	for (const header &h : m_headers)
	{
		if (not iequals(h.name, "Cookie"))
			continue;

		std::vector<std::string> rawCookies;
		split(rawCookies, h.value, ";");

		for (std::string &cookie : rawCookies)
		{
			trim(cookie);

			auto d = cookie.find('=');
			if (d == std::string::npos)
				continue;

			if (cookie.compare(0, d, name) == 0)
				return cookie.substr(d + 1);
		}
	}

	return "";
}

void request::set_cookie(const std::string &name, std::string value)
{
	std::map<std::string, std::string> cookies;
	for (auto &h : m_headers)
	{
		if (not iequals(h.name, "Cookie"))
			continue;

		std::vector<std::string> rawCookies;
		split(rawCookies, h.value, ";");

		for (std::string &cookie : rawCookies)
		{
			trim(cookie);

			auto d = cookie.find('=');
			if (d == std::string::npos)
				continue;

			cookies[cookie.substr(0, d)] = cookie.substr(d + 1);
		}
	}

	std::erase_if(m_headers, [](header &h)
		{ return iequals(h.name, "Cookie"); });

	cookies[name] = std::move(value);

	std::ostringstream cs;
	bool first = true;
	for (auto &cookie : cookies)
	{
		if (first)
			first = false;
		else
			cs << "; ";

		cs << cookie.first << '=' << cookie.second;
	}

	set_header("Cookie", cs.str());
}

// --------------------------------------------------------------------
// Locale support

class locale_table
{
  public:
	static locale_table &instance()
	{
		static locale_table s_instance;
		return s_instance;
	}

	std::locale get(const std::string &accept_language);

  private:
	locale_table() = default;

	static const std::map<std::string_view, std::vector<std::string_view>> kLocalesPerLang;
	static const std::regex kAcceptsRX;
};

const std::map<std::string_view, std::vector<std::string_view>>
	locale_table::kLocalesPerLang{
		{ "ar", { "AE", "BH", "DZ", "EG", "IQ", "JO", "KW", "LB", "LY", "MA", "OM", "QA", "SA", "SD", "SY", "TN", "YE" } },
		{ "be", { "BY" } },
		{ "bg", { "BG" } },
		{ "ca", { "ES" } },
		{ "cs", { "CZ" } },
		{ "da", { "DK" } },
		{ "de", { "AT", "CH", "DE", "LU" } },
		{ "el", { "GR" } },
		{ "en", { "US", "AU", "CA", "GB", "IE", "IN", "NZ", "ZA" } },
		{ "es", { "AR", "BO", "CL", "CO", "CR", "DO", "EC", "ES", "GT", "HN", "MX", "NI", "PA", "PE", "PR", "PY", "SV", "UY", "VE" } },
		{ "et", { "EE" } },
		{ "fi", { "FI" } },
		{ "fr", { "BE", "CA", "CH", "FR", "LU" } },
		{ "hi", { "IN" } },
		{ "hr", { "HR" } },
		{ "hu", { "HU" } },
		{ "is", { "IS" } },
		{ "it", { "CH", "IT" } },
		{ "iw", { "IL" } },
		{ "ja", { "JP" } },
		{ "ko", { "KR" } },
		{ "lt", { "LT" } },
		{ "lv", { "LV" } },
		{ "mk", { "MK" } },
		{ "nl", { "NL", "BE" } },
		{ "no", { "NO", "NO_NY" } },
		{ "pl", { "PL" } },
		{ "pt", { "BR", "PT" } },
		{ "ro", { "RO" } },
		{ "ru", { "RU" } },
		{ "sk", { "SK" } },
		{ "sl", { "SI" } },
		{ "sq", { "AL" } },
		{ "sr", { "BA", "CS" } },
		{ "sv", { "SE" } },
		{ "th", { "TH", "TH_TH" } },
		{ "tr", { "TR" } },
		{ "uk", { "UA" } },
		{ "vi", { "VN" } },
		{ "zh", { "CN", "HK", "TW" } }
	};

const std::regex locale_table::kAcceptsRX(R"(([[:alpha:]]{1,8})(?:-([[:alnum:]]{1,8}))?(?:;q=([01](?:\.\d{1,3})))?)");

std::locale locale_table::get(const std::string &acceptedLanguage)
{
	std::vector<std::string> accepted;
	split(accepted, acceptedLanguage, ",");

	struct lang_score
	{
		std::string lang, region;
		float score;
		std::locale loc;
		bool operator<(const lang_score &rhs) const
		{
			return score > rhs.score;
		}
	};

	std::vector<lang_score> scores;

	auto tryLangRegion = [&scores](std::string lang, std::string region, float score)
	{
		try
		{
			auto name = lang + '_' + region + ".UTF-8";
			std::locale loc(name);
			if (iequals(loc.name(), name))
				scores.emplace_back(std::move(lang), std::move(region), score, loc);
		}
		catch (const std::exception &) // NOLINT(bugprone-empty-catch)
		{
		}
	};

	for (auto &l : accepted)
	{
		std::smatch m;
		if (std::regex_search(l, m, kAcceptsRX))
		{
			float score = 1;
			if (m[3].matched)
				score = std::stof(m.str(3));

			auto lang = m.str(1);

			if (m[2].matched)
				tryLangRegion(lang, m[2], score);
			else if (kLocalesPerLang.count(lang))
			{
				for (auto region : kLocalesPerLang.at(lang))
					tryLangRegion(lang, std::string{ region }, score);
			}
		}
	}

	return scores.empty() ? std::locale("C") : std::locale(scores.front().loc);
}

std::locale request::get_locale() const
{
	return locale_table::instance().get(get_header("Accept-Language"));
}

namespace
{
	const std::string_view
		kNameValueSeparator{ ": " },
		kCRLF{ "\r\n" };
}

std::vector<std::string_view> request::to_buffers() const
{
	thread_local static std::string s_request_line;

	std::vector<std::string_view> result;

	s_request_line = get_request_line();

	result.emplace_back(s_request_line);
	result.emplace_back(kCRLF);

	for (const header &h : m_headers)
	{
		result.emplace_back(h.name);
		result.emplace_back(kNameValueSeparator);
		result.emplace_back(h.value);
		result.emplace_back(kCRLF);
	}

	result.emplace_back(kCRLF);
	result.emplace_back(m_payload);

	return result;
}

// std::vector<asio_ns::const_buffer> request::to_buffers() const
// {
// 	thread_local static std::string s_request_line;

// 	std::vector<asio_ns::const_buffer> result;

// 	s_request_line = get_request_line();

// 	result.emplace_back(asio_ns::buffer(s_request_line));
// 	result.push_back(asio_ns::buffer(kCRLF));

// 	for (const header &h : m_headers)
// 	{
// 		result.push_back(asio_ns::buffer(h.name));
// 		result.push_back(asio_ns::buffer(kNameValueSeparator));
// 		result.push_back(asio_ns::buffer(h.value));
// 		result.push_back(asio_ns::buffer(kCRLF));
// 	}

// 	result.push_back(asio_ns::buffer(kCRLF));
// 	result.push_back(asio_ns::buffer(m_payload));

// 	return result;
// }

std::ostream &operator<<(std::ostream &io, const request &req)
{
	io << req.get_request_line() << "\r\n";

	for (const header &h : req.m_headers)
		io << h.name << ": " << h.value << "\r\n";

	io << "\r\n" << req.m_payload;

	return io;
}

} // namespace zeep::http
