#include "okapi.h"
#include "debug.h"

#include <iostream>
#include <iomanip>
#include <sstream>
#include <algorithm>

#include <curl/curl.h>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

Okapi::Okapi(const Service serv) : service(serv) {
#include "config_user.h"

	switch (serv) {
		case ocpl:
			url = ocpl_url;
			key = ocpl_key;
			break;
		case ocde:
			url = ocde_url;
			key = ocde_key;
			break;
		case ocus:
			url = ocus_url;
			key = ocus_key;
			break;
		case ocnl:
			url = ocnl_url;
			key = ocnl_key;
			break;
		case ocro:
			url = ocro_url;
			key = ocro_key;
			break;
		case ocuk:
			url = ocuk_url;
			key = ocuk_key;
			break;
		case none:
			throw 1;
	}

	curl = curl_easy_init();
	if (!curl) {
		curl_global_cleanup();
		throw 0;
	}

	curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&curl_output);
	curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5);
}

Okapi::~Okapi() {
	curl_easy_cleanup(curl);
}

// Callback for cURL easy interface used to save received output to std::string object
size_t Okapi::write_cb(char* ptr, __attribute__((unused)) size_t size, size_t nmemb, void* userdata) {
	std::string* str = reinterpret_cast<std::string*>(userdata);
	str->append(ptr, nmemb);
	return nmemb;
}

void Okapi::curl_post(const std::string& url, const std::string& post) const {
	CURLcode res;

	Debug(5) << "API query: " << post;

	curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str());
	curl_output.clear();

	res = curl_easy_perform(curl);
	if (res != CURLE_OK) {
		Debug(1) << "Connection failed: " << curl_easy_strerror(res) << '\n';
		curl_easy_cleanup(curl);
		std::exit(EXIT_FAILURE);
	}

	Debug(5) << "API query result: " << curl_output;
	if (curl_output.starts_with("{\"error\"")) {
		json j = json::parse(curl_output);
		Debug(1) << "OKAPI error: " << j["error"]["developer_message"];
		std::exit(EXIT_FAILURE);
	}
}

void Okapi::get_user_caches_json(const std::string& uuid, int count, int offset) const {
	std::string api_service = url + OKAPI_logs;
	std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=cache_code|type|date|was_recommended&limit=" + std::to_string(count) + "&offset=" + std::to_string(offset);
	curl_post(api_service, query);
}

// std::string Okapi::get_cache_json(std::string code) {
// 	std::string service = url + OKAPI_cache;
// 	std::string query = "consumer_key=" + key + "&cache_code=" + code + "&fields=code|name|location|type|status|difficulty|terrain";
// 	return curl_post(service, query);
// }

void Okapi::get_caches_json(const std::string& codes) const {
	std::string api_service = url + OKAPI_caches;
	std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|name|location|type|status|difficulty|terrain|owner|region|country2|size2|date_hidden|date_created|recommendations|rating|founds|status|internal_id";
	curl_post(api_service, query);
}

// void Okapi::get_caches_ratings_json(const std::string& codes) const {
// 	std::string api_service = url + OKAPI_caches;
// 	std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|recommendations|rating|status";
// 	curl_post(api_service, query);
// }

// Cache Okapi::get_cache(std::string code) {
// 	std::string output = get_cache_json(code);
// 	json j = json::parse(output);
//
// 	Cache c;
// 	c.code = code;
// 	c.name = j["name"];
// 	c.type = j["type"];
// 	c.diff = j["difficulty"].get<float>();
// 	c.terr = j["terrain"].get<float>();
// // 	std::cout << j["difficulty"] << '\n';
// // 	std::cout << j["difficulty"].get<float>() << '\n';
// // 	std::cout << j["difficulty"].get<double>() << '\n';
// // 	std::cout << j["difficulty"].get<int>() << '\n';
//
// 	std::string loc = j["location"];
// 	int pos = loc.find("|");
// 	//c.pos.lat = stof(loc.substr(0, pos));
// 	//c.pos.lon = stof(loc.substr(pos + 1));
//
// 	return c;
// }

Caches Okapi::get_caches(const std::set<std::string>& codes) const {
	Cache c;
	Caches cc;

	uint k;
	std::string codes_list;
	codes_list.reserve(MAX_CACHES * 8); // maximum of MAX_CACHES codes, 7 chars per code plus a separator

	auto it = codes.begin();
	while (it != codes.end()) {
		k = 0;

		while (it != codes.end() && k < MAX_CACHES) {
			codes_list += *it;
			codes_list += '|';
			it++;
			k++;
		}
		codes_list.pop_back(); // remove trailing '|'

		get_caches_json(codes_list);
		json j = json::parse(curl_output);
		codes_list.clear();

		for (auto& el : j.items()) {
			if (el.value().is_null()) continue;
			c.code = el.value()["code"];
			if (el.value()["internal_id"].is_number())
				c.internal_id = el.value()["internal_id"];
			else
				c.internal_id = std::stoi(el.value()["internal_id"].get<std::string>());
			c.name = el.value()["name"];
			c.type = el.value()["type"];
			c.size = el.value()["size2"];
			c.region = el.value()["region"];
			c.country = el.value()["country2"];
			c.diff = el.value()["difficulty"];
			c.terr = el.value()["terrain"];
			c.owner = el.value()["owner"]["username"];
			c.owner_uuid = el.value()["owner"]["uuid"];
			c.pos = Position(el.value()["location"].get<std::string>());
			c.origin = service;
			c.fav = el.value()["recommendations"];
			if (!el.value()["rating"].is_null()) c.rating = el.value()["rating"];
			c.founds = el.value()["founds"];

			if (el.value()["status"] == "Available")
				c.status = ok;
			else if (el.value()["status"] == "Temporarily unavailable")
				c.status = disabled;
			else if (el.value()["status"] == "Archived")
				c.status = archived;
			else
				c.status = unknown;

			std::tm tmp;
			std::stringstream ss(std::max(el.value()["date_hidden"].get<std::string>(), el.value()["date_created"].get<std::string>()));
			ss >> std::get_time(&tmp, "%Y-%m-%dT%H:%M:%S+");
			tmp.tm_isdst = -1;
			c.set_date_hidden(tmp);

			cc.push_back(c);
		}
	}
	return cc;
}

Caches Okapi::get_user_caches(const std::string& uuid, int count) const {
	Caches cc;
	std::set<std::string> codes;

	struct extra_data {
		std::tm date;
		bool was_recommended;
	};
	std::map<std::string, extra_data> tmp_data; // holds data from logs which needs to be added to Caches later

	json j;
	std::tm date;
	int off = 0;

	if (count == 0)
		do {
			get_user_caches_json(uuid, MAX_LOGS, off);
			j = json::parse(curl_output);

			for (auto& el : j.items()) {
				if (el.value()["type"] == "Found it" || el.value()["type"] == "Attended") {
					std::stringstream ss(el.value()["date"].get<std::string>());
					// TODO need to take care of the time zone :/
					ss >> std::get_time(&date, "%Y-%m-%dT%H:%M:%S+");
					date.tm_isdst = -1;
					codes.insert(el.value()["cache_code"].get<std::string>());
					tmp_data[el.value()["cache_code"].get<std::string>()] = { date, el.value()["was_recommended"] };
				}
			}
			off += j.size();
		} while (j.size() > 0);
	else {
		int count_req = (count > MAX_LOGS) ? MAX_LOGS : count;
		do {
			get_user_caches_json(uuid, count_req, off);
			j = json::parse(curl_output);

			for (auto& el : j.items()) {
				if (el.value()["type"] == "Found it" || el.value()["type"] == "Attended") {
					std::stringstream ss(el.value()["date"].get<std::string>());
					ss >> std::get_time(&date, "%Y-%m-%dT%H-%M-%S");
					date.tm_isdst = -1;
					codes.insert(el.value()["cache_code"].get<std::string>());
					tmp_data[el.value()["cache_code"].get<std::string>()] = { date, el.value()["was_recommended"] };
				}
			}
			off += j.size();
			count -= count_req;
		} while (j.size() > 0 && count > 0);
	}

	cc = get_caches(codes);
	Debug(2) << "Caches read from OC: " << cc.size() << '\n';
	std::map<std::string, Cache&> mcc;
	for (auto& i : cc)
		mcc.insert({ i.code, i });

	// introduce extra data from the logs to Caches
	for (const auto& el : tmp_data) {
		auto& it = mcc.at(el.first);
		it.set_date(el.second.date);
		it.recommended = el.second.was_recommended;
	}

	return cc;
}

int Okapi::get_user_hidden_caches_no(const std::string& uuid) const {
	std::string api_service = url + OKAPI_user;
	std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=caches_hidden";
	curl_post(api_service, query);
	json j = json::parse(curl_output);
	return j["caches_hidden"];
}

void Okapi::update_caches(Caches& cc) const {
	std::set<std::string> codes;
	for (auto& i : cc)
		codes.insert(i.code);
	cc = get_caches(codes);
}

// void Okapi::update_caches_ratings(Caches& cc) const {
// 	std::map<std::string, Cache*> pcc;
// 	std::string code;
//
// 	uint k;
// 	std::string codes_list;
// 	codes_list.reserve(MAX_CACHES * 8); // maximum of MAX_CACHES codes, 7 chars per code plus a separator
//
// 	auto it = cc.begin();
// 	while (it != cc.end()) {
// 		k = 0;
//
// 		while (it != cc.end() && k < MAX_CACHES) {
// 			codes_list += it->code;
// 			codes_list += '|';
// 			pcc[it->code] = &*it;
// 			it++;
// 			k++;
// 		}
// 		codes_list.pop_back(); // remove trailing '|'
//
// 		get_caches_ratings_json(codes_list);
// 		json j = json::parse(curl_output);
// 		codes_list.clear();
//
// 		for (auto& el : j.items()) {
// 			code = el.value()["code"];
// 			pcc[code]->fav = el.value()["recommendations"];
// 			if (!el.value()["rating"].is_null()) pcc[code]->rating = el.value()["rating"];
//
// 			if (el.value()["status"] == "Available")
// 				pcc[code]->status = ok;
// 			else if (el.value()["status"] == "Temporarily unavailable")
// 				pcc[code]->status = disabled;
// 			else if (el.value()["status"] == "Archived")
// 				pcc[code]->status = archived;
// 			else
// 				pcc[code]->status = unknown;
// 		}
// 	}
// }

std::string Okapi::get_uuid(const std::string& username, uint* id) const {
	std::string api_service = url + OKAPI_username;
	char* user_esc = curl_easy_escape(curl, username.c_str(), username.size());
	std::string query = "consumer_key=" + key + "&username=" + user_esc + "&fields=uuid|internal_id";
	curl_free(user_esc);
	curl_post(api_service, query);
	json j = json::parse(curl_output);
	if (id) *id = j["internal_id"];
	return j["uuid"];
}

std::string Okapi::get_profile_url(const std::string& uuid) const {
	std::string api_service = url + OKAPI_user;
	std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=profile_url";
	curl_post(api_service, query);
	json j = json::parse(curl_output);
	return j["profile_url"];
}

std::string Okapi::get_changelog_json(int revision) const {
	std::string api_service = url + OKAPI_changelog;
	std::string query = "consumer_key=" + key + "&since=" + std::to_string(revision);
	curl_post(api_service, query);
	return curl_output;
}

void Okapi::get_ftf(uint uid, Caches& cc) const {
	std::string ftf_url = "https://opencaching.pl/UserProfile/getUserFtfsAjax/" + std::to_string(uid);
	CURLcode res;

	curl_easy_setopt(curl, CURLOPT_URL, ftf_url.c_str());
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
	curl_output.clear();

	res = curl_easy_perform(curl);
	if (res != CURLE_OK) {
		Debug(1) << "Connection failed: " << curl_easy_strerror(res) << '\n';
		curl_easy_cleanup(curl);
		std::exit(EXIT_FAILURE);
	}

	Debug(5) << "FTF query result: " << curl_output;
	json j = json::parse(curl_output);
	for (auto& el : j.items()) {
		if (el.value().is_null()) continue;
		uint id = el.value()["cache_id"];
		auto c = std::find_if(cc.begin(), cc.end(), [&](const auto& a) { return a.internal_id == id; });
		if (c != std::end(cc))
			c->ftf = 1;
	}
}
