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

#include <string>
#include <fstream>
#include <map>
#include <sqlite3.h>
#include <nlohmann/json.hpp>

using json = nlohmann::json;
// using namespace std::literals::string_literals;

OCdb::OCdb(const std::string& db_file) {
	int res = 0;

	res = sqlite3_open(db_file.c_str(), &db);
	if (res != SQLITE_OK) {
		Debug(1) << sqlite3_errmsg(db);
		throw 1;
	}
	if (!request("CREATE TABLE IF NOT EXISTS revision (revision INTEGER PRIMARY KEY);") ||
		!request("CREATE TABLE IF NOT EXISTS caches (code TEXT PRIMARY KEY, name TEXT, location TEXT, type TEXT, status TEXT, size TEXT, difficulty REAL, terrain REAL, country TEXT, region TEXT, owner TEXT);") ||
		!request("CREATE TABLE IF NOT EXISTS logs (uuid TEXT PRIMARY KEY, cache_code TEXT, date TEXT, user TEXT, type TEXT);"))
		throw 1;
	if (!read_revision()) {
		Debug(1) << "Error reading database revision, database may be corrupt or empty.\n";
	}
}

OCdb::~OCdb() {
	sqlite3_close(db);
}

bool OCdb::request(const std::string& req) const {
	int res;

	res = sqlite3_prepare_v2(db, req.c_str(), req.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << req << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	res = sqlite3_step(stmt);
	if (res != SQLITE_DONE) {
		Debug(1) << "Request \"" << req << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	sqlite3_finalize(stmt);
	return 1;
}

bool OCdb::init(const std::string& dump_path) {
	request("DELETE FROM caches;");
	request("DELETE FROM logs;");
	request("DELETE FROM revision;");

	std::ifstream file(dump_path + "index.json");
	json j;
	file >> j;
	revision = j["revision"];
	request("BEGIN TRANSACTION;");
	request("INSERT INTO revision VALUES (" + std::to_string(revision) + ");");

	for (auto& el : j["data_files"].items()) {
		init_part(dump_path + el.value().get<std::string>());
	}
	request("COMMIT;");
	return 1;
}

bool OCdb::init_part(const std::string& json_file) {
	Debug(2) << "Processing file: " << json_file << '\n';

	std::ifstream file(json_file);
	json j;
	file >> j;

	Debug(5) << "File: " << json_file << " read.\n";
	for (auto& el : j.items()) {
		parse_item(el.value());
	}
	return 1;
}

bool OCdb::update(const Okapi& oc) {
	if (revision == 0) {
		Debug(1) << "Cannot update database, you need to init it first with a full dump.\n";
		throw 1;
	}
	Debug(2) << "Fetching changelog since revision " << revision << ".\n";
	std::string output = oc.get_changelog_json(revision);
	json j = json::parse(output);

	request("BEGIN TRANSACTION;");
	for (auto& el : j["changelog"].items()) {
		parse_item(el.value());
	}
	set_revision(j["revision"]);
	request("COMMIT;");
	if (j["more"])
		update(oc);
	return 1;
}

bool OCdb::parse_item(const json& j) {
	if (j.count("object_type") > 0 && j["object_type"] == "geocache") {
		if (j.count("change_type") > 0 && j["change_type"] == "replace") {
			Debug(5) << "Inserting/updating cache " << j["object_key"]["code"].get<std::string>() << ".\n";
			// if (j["object_key"]["code"] != j["data"]["code"]) {
			// 	Debug(1) << "Code change " << j["object_key"]["code"] << " -> " << j["data"]["code"] <<'\n';
			// }
			update_cache(j);
		} else if (j.count("change_type") > 0 && j["change_type"] == "delete") {
			Debug(2) << "Deleting cache " << j["object_key"]["code"].get<std::string>() << ".\n";
			std::string sql = "DELETE FROM caches WHERE code='" + j["object_key"]["code"].get<std::string>() + "';";
			request(sql);
		} else {
			Debug(1) << "Incorrect change type: " << j["change_type"] << ".\n";
		}
	} else if (j.count("object_type") > 0 && j["object_type"] == "log") {
		if (j["change_type"] == "replace") {
			Debug(3) << "Updating log " << j["object_key"]["uuid"] << ".\n";
			update_log(j);
		} else if (j.count("change_type") > 0 && j["change_type"] == "delete") {
			Debug(2) << "Deleting log " << j["object_key"]["uuid"] << ".\n";
			std::string sql = "DELETE FROM logs WHERE uuid='" + j["object_key"]["uuid"].get<std::string>() + "';";
			request(sql);
		} else {
			Debug(1) << "Incorrect change type: " << j["change_type"] << ".\n";
		}
	} else {
		Debug(1) << "Incorrect object type: " << j["object_type"] << ".\n";
	}
	return 1;
}

bool OCdb::update_cache(const json& j) {
	// (code TEXT PRIMARY KEY, name TEXT, location TEXT, type TEXT, status TEXT, size TEXT, difficulty REAL, terrain REAL, country TEXT, region TEXT, owner TEXT)
	std::map<std::string, std::string> fields;
	std::map<std::string, float> fields2;
	int res;

	std::string code = j["object_key"]["code"].get<std::string>();
	if (j["data"]["names"].count("pl") > 0 && !j["data"]["names"]["pl"].is_null())
		fields["name"] = j["data"]["names"]["pl"].get<std::string>();
	else if (j["data"]["names"].count("en") > 0 && !j["data"]["names"]["en"].is_null())
		fields["name"] = j["data"]["names"]["en"].get<std::string>();
	if (j["data"].count("location") > 0 && !j["data"]["location"].is_null())
		fields["location"] = j["data"]["location"].get<std::string>();
	if (j["data"].count("type") > 0 && !j["data"]["type"].is_null())
		fields["type"] = j["data"]["type"].get<std::string>();
	if (j["data"].count("status") > 0 && !j["data"]["status"].is_null())
		fields["status"] = j["data"]["status"].get<std::string>();
	if (j["data"].count("size2") > 0 && !j["data"]["size2"].is_null())
		fields["size"] = j["data"]["size2"].get<std::string>();
	if (j["data"].count("difficulty") > 0 && !j["data"]["difficulty"].is_null())
		fields2["difficulty"] = j["data"]["difficulty"].get<float>();
	if (j["data"].count("terrain") > 0 && !j["data"]["terrain"].is_null())
		fields2["terrain"] = j["data"]["terrain"].get<float>();
	if (j["data"].count("country") > 0 && !j["data"]["country"].is_null())
		fields["country"] = j["data"]["country"].get<std::string>();
	if (j["data"].count("region") > 0 && !j["data"]["region"].is_null())
		fields["region"] = j["data"]["region"].get<std::string>();
	else if (j["data"].count("state") > 0 && !j["data"]["state"].is_null())
		fields["region"] = j["data"]["state"].get<std::string>();
	if (j["data"]["owner"].count("uuid") > 0 && !j["data"]["owner"]["uuid"].is_null())
		fields["owner"] = j["data"]["owner"]["uuid"].get<std::string>();

	if (fields.empty() && fields2.empty())
		return 1;

	std::string sql = "INSERT INTO caches (code,";
	for (auto& i : fields)
		sql += i.first + ',';
	for (auto& i : fields2)
		sql += i.first + ',';
	sql.pop_back();
	sql += ") VALUES ('" + code + "',";
	for (__attribute__((unused)) auto& i : fields)
		sql += "?,";
	for (__attribute__((unused)) auto& i : fields2)
		sql += "?,";
	sql.pop_back();
	sql += ") ON CONFLICT(code) DO UPDATE SET ";
	for (auto& i : fields)
		sql += i.first + "=excluded." + i.first + ',';
	for (auto& i : fields2)
		sql += i.first + "=excluded." + i.first + ',';
	sql.pop_back();
	sql += ';';

	res = sqlite3_prepare_v2(db, sql.c_str(), sql.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	int n = 1;
	for (auto& i : fields) {
		sqlite3_bind_text(stmt, n++, i.second.c_str(), -1, nullptr);
	}
	for (auto& i : fields2) {
		sqlite3_bind_double(stmt, n++, i.second);
	}
	res = sqlite3_step(stmt);
	if (res != SQLITE_DONE) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	sqlite3_finalize(stmt);
	return 1;
}

bool OCdb::update_log(const json& j) {
	// logs (uuid TEXT PRIMARY KEY, cache_code TEXT, date TEXT, user TEXT, type TEXT);"))
	std::map<std::string, std::string> fields;
	int res;

	std::string uuid = j["object_key"]["uuid"].get<std::string>();
	if (j["data"].count("cache_code") > 0 && !j["data"]["cache_code"].is_null())
		fields["cache_code"] = j["data"]["cache_code"].get<std::string>();
	if (j["data"].count("date") > 0 && !j["data"]["date"].is_null())
		fields["date"] = j["data"]["date"].get<std::string>();
	if (j["data"]["user"].count("uuid") > 0 && !j["data"]["user"]["uuid"].is_null())
		fields["user"] = j["data"]["user"]["uuid"].get<std::string>();
	if (j["data"].count("type") > 0 && !j["data"]["type"].is_null())
		fields["type"] = j["data"]["type"].get<std::string>();

	if (fields.empty())
		return 1;

	std::string sql = "INSERT INTO logs (uuid,";
	for (auto& i : fields)
		sql += i.first + ',';
	sql.pop_back();
	sql += ") VALUES ('" + uuid + "',";
	for (__attribute__((unused)) auto& i : fields)
		sql += "?,";
	sql.pop_back();
	sql += ") ON CONFLICT(uuid) DO UPDATE SET ";
	for (auto& i : fields)
		sql += i.first + "=excluded." + i.first + ',';
	sql.pop_back();
	sql += ';';

	res = sqlite3_prepare_v2(db, sql.c_str(), sql.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	int n = 1;
	for (auto& i : fields) {
		sqlite3_bind_text(stmt, n++, i.second.c_str(), -1, nullptr);
	}
	res = sqlite3_step(stmt);
	if (res != SQLITE_DONE) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	sqlite3_finalize(stmt);
	return 1;
}

bool OCdb::read_revision() {
	int res;
	std::string sql = "SELECT revision FROM revision;";

	res = sqlite3_prepare_v2(db, sql.c_str(), sql.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	res = sqlite3_step(stmt);
	if (res != SQLITE_ROW) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		return 0;
	}
	revision = sqlite3_column_int(stmt, 0);
	sqlite3_finalize(stmt);
	Debug(2) << "Revision: " << revision << '\n';

	return 1;
}

Caches OCdb::get_user_caches_not_found(const std::string& uuid) const {
	int res;
	Caches cc;
	Cache c;

	std::string sql = "SELECT code, location FROM caches WHERE status = 'Available' AND NOT EXISTS (SELECT cache_code FROM logs WHERE cache_code = code AND type = 'Found it' and user = ?);";

	res = sqlite3_prepare_v2(db, sql.c_str(), sql.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		throw 0;
	}
	sqlite3_bind_text(stmt, 1, uuid.c_str(), -1, nullptr);

	res = sqlite3_step(stmt);
	while (res == SQLITE_ROW) {
		c.code = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
		c.pos = get_lat_lon(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
		cc.push_back(c);
		res = sqlite3_step(stmt);
	}

	if (res != SQLITE_DONE) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		throw 0;
	}
	sqlite3_finalize(stmt);
	return cc;
}

Caches OCdb::get_user_caches(const std::string& uuid, __attribute__((unused)) int count) const {
	int res;
	Caches cc;
	Cache c;
	//code TEXT PRIMARY KEY, name TEXT, location TEXT, type TEXT, status TEXT, size TEXT, difficulty REAL, terrain REAL, country TEXT, region TEXT, owner TEXT)
	std::string sql = "SELECT code, location, type, size, difficulty, terrain, country, region, owner FROM caches WHERE EXISTS (SELECT cache_code FROM logs WHERE cache_code = code AND type = 'Found it' and user = ?);";

	res = sqlite3_prepare_v2(db, sql.c_str(), sql.length() + 1, &stmt, NULL);
	if (res != SQLITE_OK) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		throw 0;
	}
	sqlite3_bind_text(stmt, 1, uuid.c_str(), -1, nullptr);

	res = sqlite3_step(stmt);
	while (res == SQLITE_ROW) {
		c.code = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
		c.pos = get_lat_lon(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
		c.type = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
		c.size = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
		c.diff = sqlite3_column_double(stmt, 4);
		c.terr = sqlite3_column_double(stmt, 5);
		//c.country = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
		c.region = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
		c.owner_uuid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 8)); // TODO: we don't know owner's nick
		cc.push_back(c);
		res = sqlite3_step(stmt);
	}

	if (res != SQLITE_DONE) {
		Debug(1) << "Request \"" << sql << "\" failed:\n"
				 << sqlite3_errmsg(db);
		throw 0;
	}
	//sqlite3_finalize(stmt);
	return cc;
}

int OCdb::get_revision() const {
	return revision;
}

void OCdb::set_revision(int rev) {
	revision = rev;
	request("UPDATE revision SET revision = " + std::to_string(revision) + ';');
}
