import vibe.core.core;
import vibe.http.router;
import vibe.http.server;
import vibe.web.web;
import vibe.db.postgresql;

import mir.random : unpredictableSeedOf;
import mir.random.variable : UniformVariable;
import mir.random.engine.xorshift : Xorshift;

import std.conv : ConvException, to;
import std.array;

enum worldSize = 10000;
enum poolSize = 64;

PostgresClient client;

void main()
{
	auto connectionInfo = "host=tfb-database port=5432 "
						~ "dbname=hello_world  user=benchmarkdbuser password=benchmarkdbpass";

	client = new PostgresClient(connectionInfo, poolSize, (Connection cn){
		cn.prepareEx("fortune_prpq", "SELECT id, message::text FROM Fortune");
		cn.prepareEx("db_prpq", "SELECT randomNumber, id FROM world WHERE id = $1");
		cn.prepareEx("db_update_prpq", "UPDATE world SET randomNumber = $1  WHERE id = $2");
	} );

	{
		// Establishing each connection in the pool and performing the PREPARE procedures
		LockedConnection[poolSize] conns;

		foreach(ref c; conns)
			c = client.lockConnection();
	}

	import std.datetime : seconds;

	auto router = new URLRouter;
	router.registerWebInterface(new WebInterface);
	router.rebuild();

	auto settings = new HTTPServerSettings;
	settings.keepAliveTimeout = 20.seconds;
	settings.options |= HTTPServerOption.reusePort;
	settings.port = 8080;
	listenHTTP(settings, router);
	runEventLoop();
}

class WebInterface {
	private {
		UniformVariable!uint _uniformVariable;
		Xorshift _gen;
	}

	this()
	{
		_gen = Xorshift(unpredictableSeedOf!uint);
		_uniformVariable = UniformVariable!uint(1, worldSize);
	}

	// GET /
	void get()
	{
		render!"index.dt";
	}

	// GET /json
	void getJson(HTTPServerResponse res)
	{
		// NOTE: the status and content type parameters are optional, but we need
		// to specify them, because the default content type is "application/json; charset=UTF8"
		res.writeJsonBody(Message("Hello, World!"), HTTPStatus.ok, "application/json");
	}

	// GET /db
	void getDB(HTTPServerResponse res)
	{
		auto conn = client.lockConnection();
		scope(exit)	destroy(conn);

		int id = _uniformVariable(_gen);
		QueryParams qp;
		qp.preparedStatementName("db_prpq");
		qp.argsVariadic(id);
		immutable result = conn.execPrepared(qp).rangify.front;
		auto w = WorldResponse(id, result[0].as!PGinteger);
		res.writeJsonBody(w, HTTPStatus.ok, "application/json");
	}

	// GET /queries?queries=...
	void getQueries(HTTPServerResponse res, string queries)
	{
		import std.algorithm : min, max;
		import std.uni : isNumber;

		auto conn = client.lockConnection();
		scope(exit)	destroy(conn);

		// Convert the "queries" parameter to int and ignore any conversion errors
		// Note that you'd usually declare queries as int instead. However, the
		// test required to gracefully handle errors here.
		int count = 1;
		if (queries.length && isNumber(queries[0]))
		{
			try count = min(max(queries.to!int, 1), 500);
			catch (ConvException) {}
		}

		// assemble the response array
		scope data = new WorldResponse[count];
		QueryParams qp;
		qp.preparedStatementName("db_prpq");
		foreach (ref w; data) {
			int id = _uniformVariable(_gen);
			qp.argsVariadic(id);
			immutable result = conn.execPrepared(qp).rangify.front;
			w = WorldResponse(id, result[0].as!PGinteger);
		}

		// write response as JSON
		res.writeJsonBody(data, HTTPStatus.ok, "application/json");
	}

	// GET /fortunes
	void getFortunes()
	{
		import std.algorithm : map, sort;

		auto conn = client.lockConnection();
		scope(exit)	destroy(conn);

		FortuneResponse[] data;
		QueryParams qp;
		qp.preparedStatementName("fortune_prpq");
		auto result = conn.execPrepared(qp).rangify;
		data = result.map!(f => FortuneResponse(f[0].as!PGinteger, f[1].as!PGtext)).array;
		data ~= FortuneResponse(0, "Additional fortune added at request time.");
		data.sort!((a, b) => a.message < b.message);
		render!("fortunes.dt", data);
	}

	// GET /updates?queries=...
	void getUpdates(HTTPServerResponse res, string queries)
	{
		import std.algorithm : min, max;
		import std.uni : isNumber;

		auto conn = client.lockConnection();

		int count = 1;
		if (queries.length && isNumber(queries[0]))
		{
			try count = min(max(queries.to!int, 1), 500);
			catch (ConvException) {}
		}

		scope data = new WorldResponse[count];
		QueryParams qp;
		qp.preparedStatementName("db_prpq");

		QueryParams qp_update;
		qp_update.preparedStatementName("db_update_prpq");

		foreach (ref w; data) {
			int id = _uniformVariable(_gen);
			qp.argsVariadic(id);
			immutable result = conn.execPrepared(qp).rangify.front;
			w = WorldResponse(id, result[0].as!PGinteger);

			// update random number
			w.randomNumber = _uniformVariable(_gen);
			qp_update.argsVariadic(w.randomNumber, id);

			// persist to DB
			conn.execPrepared(qp_update);
		}

		// write response as JSON
		res.writeJsonBody(data, HTTPStatus.ok, "application/json");
	}

	// GET /plaintext
	void getPlaintext(HTTPServerResponse res)
	{
		res.writeBody("Hello, World!", HTTPStatus.ok, "text/plain");
	}
}

struct Message {
	string message;
}

struct WorldResponse {
	int id;
	int randomNumber;
}

struct FortuneResponse {
	int id;
	string message;
}
