/* Copyright 2020, Michele Santullo
 * This file is part of orotool.
 *
 * Orotool is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Orotool is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Orotool.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "private/api_nap.hpp"
#include "private/v1_endpoints.hpp"
#include "private/dateconv.hpp"
#include "private/mime_split.hpp"
#include "duckhandy/int_conv.hpp"
#include "api_nap_exception.hpp"
#include <curl_exception.h>
#include <optional>
#include <algorithm>
#include <cctype>
#include <cassert>
#if !defined(NDEBUG)
#	include <iostream>
#endif

namespace sjd = simdjson::dom;
namespace sj = simdjson;

namespace oro {

namespace {
bool equal (std::string_view a, std::string_view b) {
	return a.size() == b.size() and std::equal(
		a.begin(), a.end(),
		b.begin(),
		[](auto c1, auto c2) {return std::tolower(c1) == std::tolower(c2); }
	);
}

Header to_header (const nap::HttpResponse& resp) {
	using dhandy::int_conv;

	Header ret;
	for (const auto& entry : resp.header_list) {
		if (equal(entry.first, "Date"))
			ret.date = from_header_timestamp(std::string(entry.second));
		else if (equal(entry.first, "X-RateLimit-Limit"))
			ret.rate_limit = int_conv<decltype(ret.rate_limit)>(entry.second);
		else if (equal(entry.first, "X-RateLimit-Remaining"))
			ret.rate_limit_remaining = int_conv<decltype(ret.rate_limit_remaining)>(entry.second);
		else if (equal(entry.first, "X-RateLimit-Reset"))
			ret.rate_limit_reset = int_conv<decltype(ret.rate_limit_reset)>(entry.second);
		else if (equal(entry.first, "Retry-After"))
			ret.retry_after = int_conv<decltype(ret.retry_after)>(entry.second);
		else if (equal(entry.first, "Server"))
			ret.server = std::string(entry.second);
	}
	return ret;
}

void fill_base_json (const simdjson::dom::element& doc, BaseJsonReply& out) {
	out.generation_timestamp = std::string(doc["generation_timestamp"]);
	out.version = static_cast<int>(doc["version"].get_int64());
}

template <typename R, typename T>
std::optional<R> get_optional (const sj::simdjson_result<sjd::element>& elem) {
	typedef std::optional<R> RetType;

	T retval;
	const simdjson::error_code error = elem.get(retval);
	return (not error ? RetType{static_cast<R>(retval)} : RetType{});
}

template <typename R, typename T>
R get_or_default (const sj::simdjson_result<sjd::element>& elem, R def) {
	T retval;
	const simdjson::error_code error = elem.get(retval);
	return (not error ? static_cast<R>(retval) : def);
}

void fill_creator_list (const sjd::element& elem, std::vector<Creator>& out) {
	if (elem.is_array()) {
		auto creators = elem.get_array();
		out.reserve(creators.size());
		for (auto creator : creators) {
			Creator new_entry;
			new_entry.char_id = static_cast<unsigned int>(creator["char_id"].get_uint64());
			new_entry.name = creator["name"];
			new_entry.points = static_cast<unsigned int>(creator["points"].get_uint64());
			out.push_back(std::move(new_entry));
		}
	}
}

std::string_view header_entry (const nap::HttpResponse& resp, std::string_view name) {
	for (const auto& head : resp.header_list) {
		if (equal(head.first, name))
			return head.second;
	}
	return {""};
}

} //unnamed namespace

ApiNap::ApiNap (
	std::string&& root_address,
	std::string&& api_key,
	std::string&& client_name,
	std::string&& client_purpose
) :
	Api(std::move(root_address), std::move(api_key), std::move(client_name), std::move(client_purpose)),
	m_qrest(simdjson::SIMDJSON_PADDING)
{
	m_qrest.add_headers({
		{"X-Client", m_client_name},
		{"X-Client-Purpose", m_client_purpose},
		{"x-api-key", m_api_key}
	});
	m_qrest.set_user_agent(std::string(m_client_name));
}

ApiNap::~ApiNap() noexcept = default;

ApiOutput<Ping> ApiNap::ping(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::ping()\n";
#endif
	return fetch_and_parse<Ping>(
		g_endpoint_ping,
		[](const simdjson::dom::element& doc, Ping& out) {
			out.message = doc["message"];
		},
		with_raw
	);
}

ApiOutput<WhoAmI> ApiNap::who_am_i(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::who_am_i()\n";
#endif
	return fetch_and_parse<WhoAmI>(
		g_endpoint_whoami,
		[](const simdjson::dom::element& doc, WhoAmI& out) {
			out.master_id = static_cast<unsigned int>(doc["master_id"].get_uint64());
		},
		with_raw
	);
}

ApiOutput<Items> ApiNap::items_list(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::items_list()\n";
#endif
	return fetch_and_parse<Items>(
		g_endpoint_items_list,
		[](const simdjson::dom::element& doc, Items& out) {
			auto items = doc["items"].get_array();
			out.items.reserve(items.size());
			for (auto item : items) {
				Item new_entry;
				new_entry.item_id = static_cast<unsigned int>(item["item_id"].get_uint64());
				new_entry.unique_name = item["unique_name"];
				new_entry.name = item["name"];
				new_entry.type = item["type"].get_string();
				new_entry.subtype = get_optional<ItemSubtypeWrapper, std::string_view>(item["subtype"]);
				new_entry.npc_price = static_cast<unsigned int>(item["npc_price"].get_uint64());
				new_entry.slots = get_optional<unsigned int, uint64_t>(item["slots"]);
				out.items.push_back(std::move(new_entry));
			}
		},
		with_raw
	);
}

ApiOutput<Icons> ApiNap::items_icons(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::items_icons()\n";
#endif
	return fetch_and_parse<Icons>(
		g_endpoint_items_icons,
		[](const simdjson::dom::element& doc, Icons& out) {
			auto icons = doc["icons"].get_array();
			out.icons.reserve(icons.size());
			for (auto icon : icons) {
				Icon new_entry;
				new_entry.item_id = static_cast<unsigned int>(icon["item_id"].get_uint64());
				new_entry.icon = icon["icon"];
				out.icons.push_back(std::move(new_entry));
			}
		},
		with_raw
	);
}

ApiOutput<Shops> ApiNap::market_list(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::market_list()\n";
#endif
	return fetch_and_parse<Shops>(
		g_endpoint_market_list,
		[](const simdjson::dom::element& doc, Shops& out) {
			auto shops = doc["shops"].get_array();
			out.shops.reserve(shops.size());
			for (auto shop : shops) {
				Shop new_shop;
				new_shop.title = shop["title"];
				new_shop.owner = shop["owner"];
				new_shop.creation_date = std::string(shop["creation_date"]);
				new_shop.type = shop["type"].get_string();
				{
					auto loc = shop["location"];
					new_shop.location.map = loc["map"];
					new_shop.location.x = static_cast<int>(loc["x"].get_int64());
					new_shop.location.y = static_cast<int>(loc["y"].get_int64());
				}
				{
					auto items = shop["items"].get_array();
					new_shop.items.reserve(items.size());
					for (auto item : items) {
						ShopItem new_item;
						new_item.item_id = static_cast<unsigned int>(item["item_id"].get_uint64());
						new_item.amount = static_cast<unsigned int>(item["amount"].get_uint64());
						new_item.price = static_cast<unsigned int>(item["price"].get_uint64());
						new_item.refine = get_or_default<unsigned int, uint64_t>(item["refine"], 0);
						new_item.star_crumbs = get_or_default<unsigned int, uint64_t>(item["star_crumbs"], 0);
						new_item.element = get_optional<std::string, std::string_view>(item["element"]);
						new_item.creator = get_optional<unsigned int, uint64_t>(item["creator"]);
						new_item.beloved = get_optional<bool, bool>(item["beloved"]);

						auto item_cards = item["cards"];
						if (item_cards.is_array()) {
							auto cards = item_cards.get_array();
							new_item.cards.reserve(cards.size());
							for (uint64_t card : cards) {
								new_item.cards.push_back(static_cast<unsigned int>(card));
							}
						}

						new_shop.items.push_back(std::move(new_item));
					}
				}
				out.shops.push_back(std::move(new_shop));
			}
		},
		with_raw
	);
}

ApiOutput<Creators> ApiNap::fame_list(bool with_raw) {
#if !defined(NDEBUG)
	std::cout << "ApiNap::fame_list()\n";
#endif
	return fetch_and_parse<Creators>(
		g_endpoint_fame_list,
		[](const simdjson::dom::element& doc, Creators& out) {
			fill_creator_list(doc["brewers"], out.brewers);
			fill_creator_list(doc["forgers"], out.brewers);
		},
		with_raw
	);
}

template <typename T, typename F>
ApiOutput<T> ApiNap::fetch_and_parse (const char* endpoint, F&& data_fill, bool with_raw) {
	nap::HttpResponse resp;
	try {
		resp = m_qrest.fetch(m_prefix + endpoint);
	}
	catch (const curl::curl_easy_exception& err) {
		throw ConnectionError(err.what(), err.get_code());
	}

	if (200 != resp.code)
		throw ServerError(resp);

	{
		bool ok;
		int parsed_char;
		auto mime = tawashi::string_to_mime(header_entry(resp, "content-type"), ok, parsed_char);
		if (ok and (not equal(mime.type, "application") or not equal(mime.subtype, "json")))
			throw ContentTypeError(mime.type, mime.subtype);
	}

	T dataret;
	{
		std::unique_lock lock(m_json_mutex);
		simdjson::dom::element doc = m_json.parse(
			reinterpret_cast<const uint8_t*>(resp.body.data()),
			resp.body.size()
		);

		fill_base_json(doc, dataret);
		data_fill(doc, dataret);
	}

	std::string raw_resp;
	if (with_raw) {
		assert(not resp.header.empty() and resp.header[resp.header.size() - 1] == '\n');
		raw_resp.reserve(resp.header.size() + 1 + resp.body.size());
		raw_resp.append(resp.header);
		raw_resp.append("\n");
		raw_resp.append(resp.body);
	}

	return {to_header(resp), std::move(dataret), std::move(raw_resp)};
}

} //namespace oro
