#include "taskmanager.h"

#include <exception>
#include <sstream>
#include <boost/foreach.hpp>
#include <boost/thread/locks.hpp>

#include <curlpp/cURLpp.hpp>
#include <curlpp/Options.hpp>

namespace {
	namespace fs = boost::filesystem;
}

namespace nlp_service {

TaskManager::TaskManager(ServiceLoggerPtr logger, task_manager_config& config)
: process_path(config.get<std::string>("service.process_dir")),
  done_path(config.get<std::string>("service.done_dir")),
  tasker_url(config.get<std::string>("service.tasker_url")),
  get_method(config.get<std::string>("service.tasker_get_method")),
  finish_method(config.get<std::string>("service.tasker_finish_method")),
  logger(logger)
{
	request_headers.push_back("Accept: application/json");
	request_headers.push_back("Content-Type: application/json");
	request_headers.push_back("charsets: utf-8");
	request_data["tool"] = config.get<std::string>("service.tool");
	cURLpp::initialize();
}

TaskManager::~TaskManager()
{
	cURLpp::terminate();
}

TaskManager::task_description TaskManager::get_new_task()
{
	task_description task;
	try {
		std::string data_string = this->send_reservation();
		task = this->parse_reservation_resp(data_string);

		if(task.first.empty())
			this->logger->debug("No new task available");
	} catch (std::exception& e) {
		this->logger->error("Unable to fetch task");
		this->logger->error(e);
	}
	return task;
}

void TaskManager::finish_task(const std::string& task_token, bool success)
{
	std::string token = this->get_task_token(task_token);	// in case we got a path instead of a token
	fs::path task_process_path(this->process_path); task_process_path /= token;
	fs::path task_done_path = this->get_task_done_path(token);

	if(success && !fs::exists(task_done_path))
		this->logger->task_error(token, "Task finished, but no result file in done dir!");
	else {
		this->logger->debug("Finishing task " + token);
		try {
			std::string status_resp = this->send_response(token, success);
			this->parse_response_resp(status_resp);
			fs::remove(task_process_path);
		}
		catch (std::exception& e) {
			this->logger->error("Unable to finish task " + token);
			this->logger->error(e);
		}
	}
}

std::string TaskManager::get_task_token(const std::string& task_path)
{
	fs::path fs_task_path(task_path);
	return fs_task_path.filename().native();
}

std::string TaskManager::get_task_done_path(const std::string& task_token)
{
	std::string token = TaskManager::get_task_token(task_token);	// in case we got a path instead of token
	fs::path fs_task_path = this->done_path / token;
	return fs_task_path.native();
}

/* Sends query to the tasker in order to obtain info on new tasks available.
 */
std::string TaskManager::send_reservation()
{
	return this->send_data(this->get_method, this->request_data);
}

/* Sends information about finished task to the tasker.
 * If the task failed success should be set to false.
 */
std::string TaskManager::send_response(const std::string& task_token, bool success)
{
	std::map<std::string, std::string> data(this->request_data);
	data["task"] = task_token;
	data["success"] = success ? "True" : "False";
	return this->send_data(this->finish_method, data);
}

/* Sends arbitrary data as json to the selected tasker method
 */
std::string TaskManager::send_data(const std::string& method, const std::map<std::string, std::string>& data)
{
	std::stringstream response;

	boost::lock_guard<boost::mutex> guard(this->request_mtx);

	using namespace curlpp::options;
	this->request.reset();
	this->request.setOpt(new Url(this->tasker_url + method));
	this->request.setOpt(new HttpHeader(this->request_headers));
	this->request.setOpt(new PostFields(map_to_jsonstr(data)));

	this->request.setOpt(new WriteStream(&response));
	this->request.perform();

	return response.str();
}

/* Parses data obtained from the tasker after sending reservation (getting task)
 * and checks for errors. The errors are thrown as exceptions.
 * On success returns obtained task description.
 */
TaskManager::task_description TaskManager::parse_reservation_resp(std::string response)
{
	Json::Value response_root = this->basic_parse_resp(response, "task");

	std::string task_path = "";
	task_config task_options;

	std::string task_token = response_root.get("task", "").asString();
	if(!task_token.empty()) {
		fs::path task_fs_path(this->process_path); task_fs_path /= task_token;
		task_path = task_fs_path.native();

		std::string options_str = response_root.get("options", Json::Value::null).asString();
		Json::Value options_json; Json::Reader reader;
		reader.parse(options_str, options_json);
		task_config task_options = json_to_map(options_json);
	}

	return std::make_pair(task_path, task_options);
}

/* Parses data obtained from the tasker after sending response (finishing task)
 * and checks for errors.
 */
void TaskManager::parse_response_resp(std::string response)
{
	Json::Value response_root = this->basic_parse_resp(response, "status");

	std::string response_status = response_root["status"].asString();
	if(response_status != "OK") {
		std::string msg = "Response STATUS: " + response_root["status"].asString() + "\n" +
							"Response VALUE: " + response_root["value"].asString();
		throw std::invalid_argument(msg);
	}
}

Json::Value TaskManager::basic_parse_resp(std::string response, const std::string& check_field)
{
	Json::Value response_root;
	Json::Reader reader;
	if(!reader.parse(response, response_root))
		throw std::runtime_error("Unable to parse response JSON.");

	if(!response_root.isObject())
		throw std::invalid_argument("Response doesn't contain JSON object.");
	if(!response_root.isMember(check_field))
		throw std::invalid_argument("Response doesn't have " + check_field + " field.");

	return response_root;
}

/* Translates an arbirary map to JSON encoded string.
 */
template<typename K, typename V>
std::string TaskManager::map_to_jsonstr(const std::map<K, V>& dict)
{
	std::stringstream data;
	data << "{";

	typedef typename std::map<K, V>::const_iterator map_iterator;
	for(map_iterator iter = dict.begin(); iter != dict.end(); ++iter) {
		if(iter != dict.begin())
			data << ", ";
		data << "\"" << iter->first << "\": \"" << iter->second << "\"";
	}

	data << "}";
	return data.str();
}

/* Translates Json::Value from jsoncpp to task_config.
 */
TaskManager::task_config TaskManager::json_to_map(Json::Value json_object)
{
	if(!json_object.isObject())
		throw std::invalid_argument("Invalid JSON value: value has to be object.");

	task_config result;
	BOOST_FOREACH(const std::string& name, json_object.getMemberNames()) {
		result[name] = json_object[name].toStyledString();
	}
	return result;
}

}
