#include <algorithm>
#include <boost/algorithm/string/replace.hpp>
#include <boost/bind.hpp>
#include <deque>
#include <iostream>

#if !defined(_WINDOWS)
#include <sys/time.h>
#endif

#include "asserts.hpp"
#include "base64.hpp"
#include "filesystem.hpp"
#include "foreach.hpp"
#include "formatter.hpp"
#include "json_parser.hpp"
#include "md5.hpp"
#include "module_web_server.hpp"
#include "string_utils.hpp"
#include "utils.hpp"
#include "unit_test.hpp"
#include "variant.hpp"

using boost::asio::ip::tcp;

module_web_server::module_web_server(const std::string& data_path, boost::asio::io_service& io_service, int port)
	: http::web_server(io_service, port), timer_(io_service), 
	nheartbeat_(0), data_path_(data_path)
{
	if(data_path_.empty() || data_path_[data_path_.size()-1] != '/') {
		data_path_ += "/";
	}

	if(sys::file_exists(data_file_path())) {
		data_ = json::parse_from_file(data_file_path());
	} else {
		std::map<variant, variant> m;
		data_ = variant(&m);
	}

	heartbeat();
}

void module_web_server::heartbeat()
{
	timer_.expires_from_now(boost::posix_time::seconds(1));
	timer_.async_wait(boost::bind(&module_web_server::heartbeat, this));
}

void module_web_server::handle_post(socket_ptr socket, variant doc, const http::environment& env)
{
	std::map<variant,variant> response;
	try {
		const std::string msg_type = doc["type"].as_string();
		if(msg_type == "upload_module") {
			variant module_node = doc["module"];
			const std::string module_id = module_node["id"].as_string();
			ASSERT_LOG(std::count_if(module_id.begin(), module_id.end(), isalnum) + std::count(module_id.begin(), module_id.end(), '_') == module_id.size(), "ILLEGAL MODULE ID");

			std::vector<variant> prev_versions;

			variant current_data = data_[variant(module_id)];
			if(current_data.is_null() == false) {
				const variant new_version = module_node[variant("version")];
				const variant old_version = current_data[variant("version")];
				ASSERT_LOG(new_version > old_version, "VERSION " << new_version.write_json() << " IS NOT NEWER THAN EXISTING VERSION " << old_version.write_json());
				prev_versions = current_data[variant("previous_versions")].as_list();
				current_data.remove_attr_mutation(variant("previous_versions"));
				prev_versions.push_back(current_data);
			}

			const std::string module_path = data_path_ + module_id + ".cfg";
			const std::string module_path_tmp = module_path + ".tmp";
			const std::string contents = module_node.write_json();
			
			sys::write_file(module_path_tmp, contents);
			const int rename_result = rename(module_path_tmp.c_str(), module_path.c_str());
			ASSERT_LOG(rename_result == 0, "FAILED TO RENAME FILE: " << errno);

			response[variant("status")] = variant("ok");

			{
				std::map<variant, variant> summary;
				summary[variant("previous_versions")] = variant(&prev_versions);
				summary[variant("version")] = module_node[variant("version")];
				summary[variant("name")] = module_node[variant("name")];
				summary[variant("description")] = module_node[variant("description")];
				summary[variant("author")] = module_node[variant("author")];
				summary[variant("dependencies")] = module_node[variant("dependencies")];
				summary[variant("num_downloads")] = variant(0);
				summary[variant("num_ratings")] = variant(0);
				summary[variant("sum_ratings")] = variant(0);

				std::vector<variant> reviews_list;
				summary[variant("reviews")] = variant(&reviews_list);

				if(module_node.has_key("icon")) {
					std::string icon_str = base64::b64decode(module_node["icon"].as_string());

					const std::string hash = md5::sum(icon_str);
					sys::write_file(data_path_ + "/.glob/" + hash, icon_str);

					summary[variant("icon")] = variant(hash);
				}

				data_.add_attr_mutation(variant(module_id), variant(&summary));
				write_data();
			}

		} else if(msg_type == "query_globs") {
			response[variant("status")] = variant("ok");
			foreach(const std::string& k, doc["keys"].as_list_string()) {
				const std::string data = sys::read_file(data_path_ + "/.glob/" + k);
				response[variant(k)] = variant(base64::b64encode(data));
			}
		} else if(msg_type == "rate") {
			const std::string module_id = doc["module_id"].as_string();
			variant summary = data_[module_id];
			ASSERT_LOG(summary.is_map(), "UNKNOWN MODULE ID: " << module_id);

			const int rating = doc["rating"].as_int();
			ASSERT_LOG(rating >= 1 && rating <= 5, "ILLEGAL RATING");
			summary.add_attr_mutation(variant("num_ratings"), variant(summary["num_ratings"].as_int() + 1));
			summary.add_attr_mutation(variant("sum_ratings"), variant(summary["sum_ratings"].as_int() + rating));

			if(doc["review"].is_null() == false) {
				std::vector<variant> v = summary["reviews"].as_list();
				v.push_back(doc);
				summary.add_attr_mutation(variant("reviews"), variant(&v));
			}

			response[variant("status")] = variant("ok");
		} else {
			ASSERT_LOG(false, "Unknown message type");
		}
	} catch(validation_failure_exception& e) {
		response[variant("status")] = variant("error");
		response[variant("message")] = variant(e.msg);
	}

	send_msg(socket, "text/json", variant(&response).write_json(), "");
}

void module_web_server::handle_get(socket_ptr socket, const std::string& url, const std::map<std::string, std::string>& args)
{
	std::map<variant,variant> response;
	try {
		std::cerr << "URL: (" << url << ")\n";
		response[variant("status")] = variant("error");
		if(url == "/download_module" && args.count("module_id")) {
			const std::string module_id = args.find("module_id")->second;
			const std::string module_path = data_path_ + module_id + ".cfg";
			if(sys::file_exists(module_path)) {
				std::string response = "{\nstatus: \"ok\",\nmodule: ";
				{
					const std::string contents = sys::read_file(module_path);
					response += contents;
				}

				response += "\n}";
				send_msg(socket, "text/json", response, "");

				variant summary = data_[module_id];
				if(summary.is_map()) {
					summary.add_attr_mutation(variant("num_downloads"), variant(summary["num_downloads"].as_int() + 1));
				}
				return;

			} else {
				response[variant("message")] = variant("No such module");
			}
		} else if(url == "/get_summary") {
			response[variant("status")] = variant("ok");
			response[variant("summary")] = data_;
		} else {
			response[variant("message")] = variant("Unknown path");
		}
	} catch(validation_failure_exception& e) {
		response[variant("status")] = variant("error");
		response[variant("message")] = variant(e.msg);
	}

	send_msg(socket, "text/json", variant(&response).write_json(), "");
}

std::string module_web_server::data_file_path() const
{
	return data_path_ + "/module-data.json";
}

void module_web_server::write_data()
{
	const std::string tmp_path = data_file_path() + ".tmp";
	sys::write_file(tmp_path, data_.write_json());
	const int rename_result = rename(tmp_path.c_str(), data_file_path().c_str());
		ASSERT_LOG(rename_result == 0, "FAILED TO RENAME FILE: " << errno);
}

COMMAND_LINE_UTILITY(module_server)
{
	std::string path = ".";
	int port = 23456;

	std::deque<std::string> arguments(args.begin(), args.end());
	while(!arguments.empty()) {
		const std::string arg = arguments.front();
		arguments.pop_front();
		if(arg == "--path") {
			ASSERT_LOG(arguments.empty() == false, "NEED ARGUMENT AFTER " << arg);
			path = arguments.front();
			arguments.pop_front();
		} else if(arg == "-p" || arg == "--port") {
			ASSERT_LOG(arguments.empty() == false, "NEED ARGUMENT AFTER " << arg);
			port = atoi(arguments.front().c_str());
			arguments.pop_front();
		} else {
			ASSERT_LOG(false, "UNRECOGNIZED ARGUMENT: " << arg);
		}
	}

	const assert_recover_scope recovery;
	boost::asio::io_service io_service;
	module_web_server server(path, io_service, port);
	io_service.run();
}
