declare var setInterval: any;


type type_conf =
(
	null
	|
	{
		port: int;
		verbosity: int;
		cleaning:
		{
			timeout_in_seconds: int;
			worker_interval_in_seconds: int;
		};
	}
);


type type_event =
{
	timestamp: int;
	kind: string;
	data: any;
};


type type_user =
{
	name: string;
	role: string;
};


type type_connection =
{
	client: any;
	eventqueue: Array<type_event>;
	termination: (null | int);
};


type type_id = string;


type type_model =
{
	counter: int;
	connections: Record<type_id, type_connection>;
};


type type_internal_request =
{
	id: (null | type_id);
	action: string;
	data: any;
};


type type_internal_response = any;


/**
 * the node module "irc"
 */
var nm_irc: any;


/**
 * gets the current UNIX timestamp
 */
function get_timestamp
(
): int
{
	return Math.floor(Date.now()/1000);
}


/**
 * generates a unique 8 digit long string
 */
function generate_id
(
	model: type_model
): type_id
{
	model.counter += 1;
	return model.counter.toFixed(0).padStart(8, '0');
}


/**
 * writes a message to stderr
 */
function log
(
	conf: type_conf,
	level: int,
	incident: string,
	details: Record<string, any> = {}
): void
{
	if (level <= conf.verbosity)
	{
		process.stderr.write(`-- ${incident} | ${lib_json.encode(details)}\n`);
	}
	else
	{
		// do nothing
	}
}


/**
 * updates the termination timestamp of a connection, prolonging its lifetime
 */
function connection_touch
(
	conf: type_conf,
	connection: type_connection
): void
{
	if (conf.cleaning.timeout_in_seconds !== null)
	{
		connection.termination = (get_timestamp() + conf.cleaning.timeout_in_seconds);
	}
	else
	{
		connection.termination = null;
	}
}


/**
 * gets a connection by its ID
 */
function get_connection
(
	conf: type_conf,
	model: type_model,
	id: type_id
): type_connection
{
	if (! model.connections.hasOwnProperty(id))
	{
		throw (new Error(`no connection for ID '${id}'`));
	}
	else
	{
		const connection: type_connection = model.connections[id];
		connection_touch(conf, connection);
		return connection;
	}
}


/**
 * executes a request
 */
async function execute
(
	conf: type_conf,
	model: type_model,
	internal_request: type_internal_request,
	ip_address: string
): Promise<type_internal_response>
{
	switch (internal_request.action)
	{
		default:
		{
			throw (new Error(`unhandled action '${internal_request.action}'`));
			break;
		}
		case "connect":
		{
			if (model.connections.hasOwnProperty(internal_request.id))
			{
				throw (new Error("already connected"));
			}
			else
			{
				const id: type_id = generate_id(model);
				const client = new nm_irc.Client
				(
					internal_request.data["server"],
					internal_request.data["nickname"],
					{
						"realName": "webirc",
						"userName": lib_sha256.get(ip_address).slice(0, 8),
						"channels": internal_request.data["channels"],
						"showErrors": true,
						"autoConnect": false,
					}
				);
				let connection: type_connection =
				{
					"client": client,
					"eventqueue": [],
					"termination": null,
				};
				client.addListener
				(
					"message#",
					(nick, to, text, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "message_channel",
							"data": {"channel": to, "sender": nick, "content": text}
						});
					}
				);
				client.addListener
				(
					"pm",
					(nick, text, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "message_query",
							"data": {"user_name": nick, "sender": nick, "content": text}
						});
					}
				);
				client.addListener
				(
					"names",
					(channel, users) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "userlist",
							"data": {"channel": channel, "users": Object.entries(users).map(([name, role]) => ({"name": name, "role": role}))}
						});
					}
				);
				client.addListener
				(
					"join",
					(channel, nick, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "user_joined",
							"data": {"channel": channel, "user_name": nick}
						});
					}
				);
				client.addListener
				(
					"part",
					(channel, nick, reason, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "user_parted",
							"data": {"channel": channel, "user_name": nick}
						});
					}
				);
				client.addListener
				(
					"kick",
					(channel, nick, by, reason, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "user_kicked",
							"data": {"channel": channel, "user_name": nick, "op_name": by, "reason": reason}
						});
					}
				);
				client.addListener
				(
					"quit",
					(nick, reason, channels, message) =>
					{
						connection.eventqueue.push
						({
							"timestamp": get_timestamp(),
							"kind": "user_quit",
							"data": {"name": nick, "channels": channels}
						});
					}
				);
				client.addListener
				(
					"error",
					(error) =>
					{
						log(conf, 0, "irc_error", {"reason": error.message});
					}
				);
				client.connect
				(
					3,
					() =>
					{
						model.connections[id] = connection;
						connection_touch(conf, connection);
					}
				);
				return Promise.resolve<type_internal_response>(id);
			}
			break;
		}
		case "check":
		{
			try
			{
				get_connection(conf, model, internal_request.id);
				return Promise.resolve<type_internal_response>(true);
			}
			catch (error)
			{
				return Promise.resolve<type_internal_response>(false);
			}
			break;
		}
		case "disconnect":
		{
			const connection: type_connection = get_connection(conf, model, internal_request.id);
			delete model.connections[internal_request.id];
			connection.client.disconnect("", () => {});
			return Promise.resolve<type_internal_response>(null);
			break;
		}
		case "send_channel":
		{
			const connection: type_connection = get_connection(conf, model, internal_request.id);
			connection.client.say(internal_request.data["channel"], internal_request.data["content"]);
			return Promise.resolve<type_internal_response>(null);
			break;
		}
		case "send_query":
		{
			const connection: type_connection = get_connection(conf, model, internal_request.id);
			connection.client.say(internal_request.data["receiver"], internal_request.data["content"]);
			return Promise.resolve<type_internal_response>(null);
			break;
		}
		case "fetch":
		{
			const connection: type_connection = get_connection(conf, model, internal_request.id);
			const internal_response: type_internal_response = connection.eventqueue;
			connection.eventqueue = [];
			return Promise.resolve<type_internal_response>(internal_response);
			break;
		}
	}
}


/**
 * sets up the worker for terminating old connections
 */
function setup_cleaner
(
	conf: type_conf,
	model: type_model
): Promise<void>
{
	setInterval
	(
		() =>
		{
			const now: int = get_timestamp();
			for (const [id, connection] of Object.entries(model.connections))
			{
				if ((connection.termination !== null) && (now > connection.termination))
				{
					delete model.connections[id];
					connection.client.disconnect("timeout", () => {});
					log(conf, 1, "connection terminated after timeout", {"id": id});
				}
			}
		},
		(conf.cleaning.worker_interval_in_seconds * 1000)
	);
	return Promise.resolve<void>(undefined);
}


/**
 * sets up the server, accepting HTTP request
 */
function setup_server
(
	conf: type_conf,
	model: type_model
): Promise<void>
{
	const server: lib_server.class_server = new lib_server.class_server
	(
		conf.port,
		async (input: string, metadata?: lib_server.type_metadata): Promise<string> =>
		{
			const http_request: lib_http.type_request = lib_http.decode_request(input);
			log(conf, 2, "http_request", http_request);
			const internal_request: type_internal_request = lib_json.decode(http_request.body);
			log(conf, 1, "internal_request", internal_request);
			let internal_response: type_internal_response;
			let error: (null | Error);
			try
			{
				internal_response = await execute(conf, model, internal_request, metadata.ip_address);
				error = null;
			}
			catch (error_)
			{
				internal_response = null;
				error = error_;
			}
			let http_response: lib_http.type_response;
			if (error !== null)
			{
				log(conf, 0, "error_in_execution", {"reason": error.toString()});
				http_response =
				{
					"statuscode": 500,
					"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "text/plain"},
					"body": "error executing the request; check the server logs for details",
				};
			}
			else
			{
				log(conf, 1, "internal_response", {"value": internal_response});
				http_response =
				{
					"statuscode": 200,
					"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"},
					"body": lib_json.encode(internal_response)
				}
			}
			log(conf, 2, "http_response", http_response);
			const output: string = lib_http.encode_response(http_response);
			return Promise.resolve<string>(output);
		}
	);
	server.start();
	return Promise.resolve<void>(undefined);
}


/**
 * initializes and starts the whole system
 */
async function main
(
): Promise<void>
{
	nm_irc = require("irc");
	const conf: type_conf = await lib_plankton.file.read("conf.json").then<type_conf>(lib_json.decode);
	let model: type_model =
	{
		"counter": 0,
		"connections": {},
	};
	await Promise.all([
		setup_cleaner(conf, model),
		setup_server(conf, model),
	]);
	return Promise.resolve<void>(undefined);
}


main().then(() => {}).catch((reason) => process.stderr.write(reason.toString()));